Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b7511f8bf | ||
|
|
cc2b8d3d33 | ||
|
|
48e8bc537d | ||
|
|
f808f2f535 | ||
|
|
aee78c930d | ||
|
|
5e7adfb38d | ||
|
|
033ba63762 | ||
|
|
e6eba7a2b7 | ||
|
|
723522b90d | ||
|
|
e7094014b7 | ||
|
|
82798d5c5a | ||
|
|
ef9c73501c | ||
|
|
872066e5be | ||
|
|
a41a944742 | ||
|
|
1ec5d88fa2 | ||
|
|
699855849d | ||
|
|
94f5da4013 | ||
|
|
4fd6ac890c | ||
|
|
115c81cba3 | ||
|
|
a54d998ba9 | ||
|
|
860f02b2c1 | ||
|
|
293378293d | ||
|
|
c5abb6fd64 | ||
|
|
1fb61377b9 | ||
|
|
1a1067bbb6 | ||
|
|
71d2109b2f | ||
|
|
7f08384372 | ||
|
|
7d003021d1 | ||
|
|
50a8f12c77 | ||
|
|
4ceb5cd66a | ||
|
|
c4158ee245 | ||
|
|
61c68761df | ||
|
|
c04b8b47eb | ||
|
|
ce6bba0510 | ||
|
|
9ea399ded5 | ||
|
|
9f7723d104 | ||
|
|
5b84843217 | ||
|
|
bd8decb4b3 | ||
|
|
52f43d4d61 | ||
|
|
7eaba5e3f3 | ||
|
|
124db82a0c | ||
|
|
b4d001eb26 | ||
|
|
60c51a1635 | ||
|
|
d208b0c277 | ||
|
|
2f836cd366 | ||
|
|
f7f2cf75ab | ||
|
|
3deed92dd9 | ||
|
|
d525bbb8c5 | ||
|
|
310e9b86be |
@@ -0,0 +1,92 @@
|
||||
# Compila o APK do Android no GitHub e o devolve como artefato.
|
||||
#
|
||||
# O runner entra só como máquina de build, pelo mesmo motivo do desktop: a VPS
|
||||
# tem ~2 GB de RAM livres e roda a produção, então um Gradle com o SDK do
|
||||
# Android ali dentro arrisca derrubar as chamadas de quem está usando o app.
|
||||
#
|
||||
# O APK sai **sem assinatura**. A chave de assinatura fica na VPS e não sobe
|
||||
# para cá: quem assina é /opt/ops/release-android.sh, depois de baixar este
|
||||
# artefato. Um APK assinado é instalável por qualquer um, e a chave é o que
|
||||
# prova que uma atualização veio de nós — ela não tem por que existir em duas
|
||||
# máquinas.
|
||||
name: Android APK
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags: ['android-v*']
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: '21'
|
||||
|
||||
# cache/cache-dependency-path apontando para o npm deste pacote não é só
|
||||
# otimização: sem eles o setup-node lê o campo `packageManager` do
|
||||
# package.json da raiz, tenta usar pnpm — que este workflow não instala —
|
||||
# e falha antes de rodar qualquer coisa.
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||
with:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
cache-dependency-path: packages/mobile/package-lock.json
|
||||
|
||||
# npm, não pnpm: packages/mobile fica fora do workspace de propósito
|
||||
# (ver pnpm-workspace.yaml). Ele tem o próprio package-lock.json.
|
||||
- name: Install Capacitor
|
||||
working-directory: packages/mobile
|
||||
run: npm ci
|
||||
|
||||
# Copia a config e os arquivos de public/ para dentro do projeto nativo.
|
||||
# Sem isso o APK sai com o que estiver commitado em assets/, que pode
|
||||
# estar velho — esses arquivos são gerados, não versionados.
|
||||
- name: Sync Capacitor
|
||||
working-directory: packages/mobile
|
||||
run: npx cap sync android
|
||||
|
||||
- name: Cache Gradle
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: gradle-${{ hashFiles('packages/mobile/android/**/*.gradle', 'packages/mobile/android/gradle/wrapper/gradle-wrapper.properties') }}
|
||||
restore-keys: gradle-
|
||||
|
||||
- name: Build unsigned release APK
|
||||
working-directory: packages/mobile/android
|
||||
run: ./gradlew assembleRelease --no-daemon --stacktrace
|
||||
|
||||
# O nome do arquivo muda conforme a versão do AGP (app-release-unsigned.apk
|
||||
# ou app-release.apk quando não há config de assinatura). Localiza em vez
|
||||
# de supor, e falha alto se nada saiu — um artefato vazio só apareceria
|
||||
# como erro lá na frente, na hora de assinar.
|
||||
- name: Collect APK
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p out
|
||||
found=$(find packages/mobile/android/app/build/outputs/apk/release -name '*.apk' | head -1)
|
||||
[ -n "$found" ] || { echo "::error::o build não produziu APK nenhum"; exit 1; }
|
||||
cp "$found" out/resenhacord-unsigned.apk
|
||||
ls -l out/
|
||||
|
||||
- name: Upload APK artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: resenhacord-apk-unsigned
|
||||
path: out/resenhacord-unsigned.apk
|
||||
retention-days: 14
|
||||
if-no-files-found: error
|
||||
@@ -0,0 +1,97 @@
|
||||
# Caminho rápido: só Windows x64, disparado à mão.
|
||||
#
|
||||
# O `release.yml` continua sendo o release de verdade (todas as plataformas,
|
||||
# publica release e alimenta o electron-updater). Este aqui existe para quando
|
||||
# você só quer um .exe para testar, sem marcar versão.
|
||||
#
|
||||
# Arquivo separado de propósito: o release.yml veio do upstream e recebe merges;
|
||||
# mexer nele criaria conflito a cada atualização.
|
||||
name: Build Windows (rápido)
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
arch:
|
||||
description: Arquitetura
|
||||
required: false
|
||||
default: x64
|
||||
type: choice
|
||||
options: [x64, arm64]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
# windows-2022, não windows-latest: a imagem latest traz o Visual Studio 18,
|
||||
# que o node-gyp embutido no electron-rebuild não detecta ao compilar os
|
||||
# módulos nativos. Mesma razão documentada no release.yml.
|
||||
runs-on: windows-2022
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
||||
with:
|
||||
version: 10.34.3
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||
with:
|
||||
node-version: 20
|
||||
cache: pnpm
|
||||
|
||||
# ~100MB de binários do Electron e das ferramentas do NSIS, baixados a
|
||||
# cada execução sem isto. É o maior ganho depois de cortar o arm64.
|
||||
- name: Cache Electron binaries
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~\AppData\Local\electron\Cache
|
||||
~\AppData\Local\electron-builder\Cache
|
||||
key: electron-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: electron-cache-${{ runner.os }}-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build shared package
|
||||
run: pnpm --filter @backspace/shared build
|
||||
|
||||
# O postinstall termina em `|| console.warn` para que quem não tem
|
||||
# ferramentas de build consiga instalar. No CI isso transforma falha em
|
||||
# sucesso silencioso: o instalador sai sem o módulo nativo e o app
|
||||
# compartilha tela sem som, sem erro nenhum. Então verifica-se o
|
||||
# resultado em vez de confiar no código de saída.
|
||||
- name: Verify native audio module compiled
|
||||
shell: bash
|
||||
run: |
|
||||
found=$(find node_modules/.pnpm -path '*electron-native-screenshare*' -name '*.node' 2>/dev/null | head -5)
|
||||
if [ -z "$found" ]; then
|
||||
echo "::error::electron-native-screenshare has no compiled .node — the installer would ship without system-audio capture"
|
||||
find node_modules/.pnpm -maxdepth 1 -name 'electron-native-screenshare*' -printf '%p\n' 2>/dev/null || true
|
||||
exit 1
|
||||
fi
|
||||
echo "OK:"; echo "$found"
|
||||
|
||||
- name: Compile desktop TypeScript
|
||||
working-directory: packages/desktop
|
||||
run: pnpm exec tsc
|
||||
|
||||
# --publish never: sem release, sem precisar de tag nem bump de versão.
|
||||
# O instalador sai como artefato desta execução.
|
||||
- name: Build installer
|
||||
working-directory: packages/desktop
|
||||
run: pnpm exec electron-builder --win --${{ inputs.arch || 'x64' }} --publish never
|
||||
env:
|
||||
CSC_IDENTITY_AUTO_DISCOVERY: "false"
|
||||
|
||||
- name: Upload installer
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: backspace-windows-${{ inputs.arch || 'x64' }}
|
||||
path: packages/desktop/dist-electron/*.exe
|
||||
retention-days: 14
|
||||
if-no-files-found: error
|
||||
@@ -0,0 +1,221 @@
|
||||
# Compila no GitHub, publica no Gitea.
|
||||
#
|
||||
# O GitHub entra só como máquina de build — é dele que vêm os runners Windows
|
||||
# de que o módulo nativo de áudio precisa. A distribuição fica no Gitea, que
|
||||
# serve os arquivos a qualquer um: assim o electron-updater não precisa de
|
||||
# credencial embutida no app, o que aconteceria com um repositório privado no
|
||||
# GitHub.
|
||||
name: Publicar no Gitea
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: ['v*']
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: Tag a publicar (ex. v1.1.0)
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: windows-2022
|
||||
args: --win --x64
|
||||
- os: ubuntu-latest
|
||||
args: --linux --x64
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
|
||||
- name: Install Linux build dependencies
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
libx11-dev libxtst-dev libxt-dev \
|
||||
libxkbcommon-dev libxkbcommon-x11-dev libxkbfile-dev \
|
||||
libxrandr-dev libxinerama-dev libx11-xcb-dev \
|
||||
libpipewire-0.3-dev libpulse-dev
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
||||
with:
|
||||
version: 10.34.3
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||
with:
|
||||
node-version: 20
|
||||
cache: pnpm
|
||||
|
||||
- name: Cache Electron binaries
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cache/electron
|
||||
~/.cache/electron-builder
|
||||
~\AppData\Local\electron\Cache
|
||||
~\AppData\Local\electron-builder\Cache
|
||||
key: electron-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: electron-cache-${{ runner.os }}-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build shared package
|
||||
run: pnpm --filter @backspace/shared build
|
||||
|
||||
# O postinstall termina em `|| console.warn` para quem não tem ferramentas
|
||||
# de build. No CI isso esconde falha: o instalador sairia sem captura de
|
||||
# áudio do sistema e ninguém saberia. Verifica-se o resultado.
|
||||
- name: Verify native audio module compiled
|
||||
if: runner.os == 'Windows'
|
||||
shell: bash
|
||||
run: |
|
||||
found=$(find node_modules/.pnpm -path '*electron-native-screenshare*' -name '*.node' | head -5)
|
||||
[ -n "$found" ] || { echo "::error::sem .node compilado — instalador sairia sem áudio do sistema"; exit 1; }
|
||||
echo "$found"
|
||||
|
||||
- name: Compile desktop TypeScript
|
||||
working-directory: packages/desktop
|
||||
run: pnpm exec tsc
|
||||
|
||||
# --publish never: o electron-builder não sabe enviar para o Gitea. Ele
|
||||
# gera os instaladores e o latest.yml (o índice que o app consulta), e o
|
||||
# passo seguinte faz o upload.
|
||||
- name: Build installers
|
||||
working-directory: packages/desktop
|
||||
run: pnpm exec electron-builder ${{ matrix.args }} --publish never
|
||||
env:
|
||||
CSC_IDENTITY_AUTO_DISCOVERY: "false"
|
||||
|
||||
- name: Upload to Gitea release
|
||||
shell: bash
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
GITEA_API: https://git.resenha.website/api/v1/repos/devsyncwrld/backspace
|
||||
TAG: latest
|
||||
run: |
|
||||
set -uo pipefail
|
||||
if [ -z "${GITEA_TOKEN:-}" ]; then
|
||||
echo "::error::segredo GITEA_TOKEN não configurado"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Sem -f e imprimindo o corpo: a versão anterior usava `curl -sf`, que
|
||||
# engole a resposta de erro, então uma falha aqui só aparecia como um
|
||||
# JSONDecodeError sem dizer o motivo.
|
||||
api() {
|
||||
local method=$1 path=$2; shift 2
|
||||
curl -s -w '\n%{http_code}' -X "$method" \
|
||||
-H "Authorization: token $GITEA_TOKEN" "$GITEA_API$path" "$@"
|
||||
}
|
||||
body() { sed '$d' <<<"$1"; }
|
||||
code() { tail -n1 <<<"$1"; }
|
||||
# jq em vez de python embutido: um heredoc multilinha dentro de um
|
||||
# bloco literal de YAML encerra o bloco na primeira linha sem recuo.
|
||||
json_id() { jq -r '.id // empty'; }
|
||||
|
||||
find_release() {
|
||||
local r; r=$(api GET "/releases/tags/$TAG")
|
||||
[ "$(code "$r")" = "200" ] && body "$r" | json_id || echo ""
|
||||
}
|
||||
|
||||
# O corpo do POST fica em ASCII de proposito: o shell do runner
|
||||
# Windows corrompe UTF-8 na requisicao e o Gitea recusa com
|
||||
# "invalid UTF-8 within /name" (HTTP 422).
|
||||
ID=$(find_release)
|
||||
if [ -z "$ID" ]; then
|
||||
R=$(api POST "/releases" -H 'Content-Type: application/json' \
|
||||
-d "{\"tag_name\":\"$TAG\",\"name\":\"Latest build\",\"target_commitish\":\"main\",\"body\":\"Installers published automatically by CI.\"}")
|
||||
if [ "$(code "$R")" = "201" ]; then
|
||||
ID=$(body "$R" | json_id)
|
||||
echo "release criada: $ID"
|
||||
else
|
||||
echo "criação retornou HTTP $(code "$R"): $(body "$R")"
|
||||
# O outro job da matriz pode tê-la criado no mesmo instante.
|
||||
ID=$(find_release)
|
||||
[ -n "$ID" ] || { echo "::error::não foi possível obter nem criar a release"; exit 1; }
|
||||
echo "release encontrada após corrida: $ID"
|
||||
fi
|
||||
else
|
||||
echo "release existente: $ID"
|
||||
fi
|
||||
|
||||
# Remove só os anexos que esta plataforma vai repor, para os dois jobs
|
||||
# não apagarem o trabalho um do outro.
|
||||
#
|
||||
# Padrão montado em bash puro: a versão anterior interpolava uma
|
||||
# expressão do Actions dentro de um `case`, e quando ela não casou o
|
||||
# laço passou em silêncio — a release ficou com dois latest.yml e o
|
||||
# updater serviu o antigo, dizendo que a versão nova não existia.
|
||||
R=$(api GET "/releases/$ID/assets")
|
||||
if [ "$(code "$R")" != "200" ]; then
|
||||
echo "::error::não foi possível listar os anexos — HTTP $(code "$R"): $(body "$R")"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Filtro dentro do jq, comparando o nome direto. A versão anterior
|
||||
# montava um regex e o casava contra "<id> <nome>", onde o `^` de
|
||||
# `^latest\.yml$` ancorava no início da linha — depois do id — e nunca
|
||||
# podia casar. O laço então só removia os .exe e deixava um latest.yml
|
||||
# duplicado, que é o arquivo que decide se há atualização.
|
||||
OLD=$(body "$R" | jq -r --arg os "${RUNNER_OS:-}" '
|
||||
.[]
|
||||
| select(
|
||||
if $os == "Windows"
|
||||
then (.name | endswith(".exe")) or .name == "latest.yml"
|
||||
else (.name | endswith(".AppImage")) or (.name | endswith(".deb")) or .name == "latest-linux.yml"
|
||||
end
|
||||
)
|
||||
| "\(.id) \(.name)"')
|
||||
echo "anexos desta plataforma já na release: $(printf '%s' "$OLD" | grep -c . || true)"
|
||||
if [ -n "$OLD" ]; then
|
||||
printf '%s\n' "$OLD" | while read -r aid aname; do
|
||||
[ -n "$aid" ] || continue
|
||||
echo "removendo anexo antigo: $aname"
|
||||
D=$(api DELETE "/releases/$ID/assets/$aid")
|
||||
[ "$(code "$D")" = "204" ] || echo "::warning::falha ao remover $aname — HTTP $(code "$D")"
|
||||
done
|
||||
fi
|
||||
|
||||
shopt -s nullglob
|
||||
sent=0
|
||||
for f in packages/desktop/dist-electron/*.exe \
|
||||
packages/desktop/dist-electron/*.AppImage \
|
||||
packages/desktop/dist-electron/*.deb \
|
||||
packages/desktop/dist-electron/latest*.yml; do
|
||||
name=$(basename "$f")
|
||||
R=$(api POST "/releases/$ID/assets?name=$name" -F "attachment=@$f")
|
||||
if [ "$(code "$R")" != "201" ]; then
|
||||
echo "::error::falha ao enviar $name — HTTP $(code "$R"): $(body "$R")"
|
||||
exit 1
|
||||
fi
|
||||
echo "enviado: $name"
|
||||
sent=$((sent+1))
|
||||
done
|
||||
[ "$sent" -gt 0 ] || { echo "::error::o build não produziu instaladores"; exit 1; }
|
||||
echo "$sent arquivo(s) publicados"
|
||||
|
||||
# O updater busca latest.yml pelo nome. Duas cópias com o mesmo nome
|
||||
# fazem o Gitea servir a mais antiga, e a atualização deixa de ser
|
||||
# oferecida — sem erro em lugar nenhum. Falha aqui em vez de publicar
|
||||
# uma release que parece boa e não atualiza.
|
||||
if [ "${RUNNER_OS:-}" = "Windows" ]; then
|
||||
R=$(api GET "/releases/$ID/assets")
|
||||
DUP=$(body "$R" | jq -r '[.[] | select(.name == "latest.yml")] | length')
|
||||
if [ "$DUP" != "1" ]; then
|
||||
echo "::error::a release tem $DUP cópias de latest.yml — o updater serviria a errada"
|
||||
exit 1
|
||||
fi
|
||||
echo "latest.yml: 1 cópia, como esperado"
|
||||
fi
|
||||
@@ -14,22 +14,26 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
# macOS — universal build (arm64 + x64) on Apple Silicon runner
|
||||
- os: macos-latest
|
||||
args: --mac --arm64 --x64
|
||||
# Fork: só as plataformas que o grupo usa. Windows arm64 saiu porque
|
||||
# dobrava o job mais lento (duas distribuições do Electron, módulos
|
||||
# nativos compilados duas vezes) e ninguém roda Windows em ARM.
|
||||
# Linux arm64 saiu pelo mesmo motivo. Com o arm64 fora, o passo que
|
||||
# instalava o fpm nativo perdeu a razão de existir: ele só era
|
||||
# necessário porque o fpm embutido no electron-builder é x86_64 e
|
||||
# falhava no runner arm64.
|
||||
# macOS removido neste fork: ninguém do grupo usa, e o runner macOS
|
||||
# é cobrado a 10x num repositório privado — era a plataforma mais cara
|
||||
# da matriz, compilada em toda tag para zero usuários.
|
||||
# Windows — x64 + arm64 on x64 runner.
|
||||
# Pinned to windows-2022 (VS 2022 / v17): the windows-latest image
|
||||
# ships VS "18", which the node-gyp bundled with electron-rebuild
|
||||
# cannot detect ("unknown version undefined") when compiling
|
||||
# uiohook-napi in postinstall.
|
||||
- os: windows-2022
|
||||
args: --win --x64 --arm64
|
||||
args: --win --x64
|
||||
# Linux — x64 on x64 runner
|
||||
- os: ubuntu-latest
|
||||
args: --linux --x64
|
||||
# Linux — arm64 on arm64 runner
|
||||
- os: ubuntu-24.04-arm
|
||||
args: --linux --arm64
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
@@ -59,17 +63,6 @@ jobs:
|
||||
libxkbcommon-dev libxkbcommon-x11-dev libxkbfile-dev \
|
||||
libxrandr-dev libxinerama-dev libx11-xcb-dev
|
||||
|
||||
- name: Install fpm for .deb packaging
|
||||
if: runner.os == 'Linux'
|
||||
# electron-builder bundles an x86_64-only fpm; on the arm64 runner it
|
||||
# aborts the .deb target with "cannot execute binary file: Exec format
|
||||
# error". Install fpm natively and set USE_SYSTEM_FPM (below) so both
|
||||
# arches package their .deb with a host-native fpm. ruby-dev + make are
|
||||
# needed for fpm's native gem dependencies.
|
||||
run: |
|
||||
sudo apt-get install -y ruby ruby-dev build-essential
|
||||
sudo gem install --no-document fpm
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
||||
with:
|
||||
@@ -97,7 +90,3 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CSC_IDENTITY_AUTO_DISCOVERY: "false"
|
||||
# Use the host-native fpm installed above instead of electron-builder's
|
||||
# bundled x86_64 fpm (which can't run on the arm64 runner). No-op on
|
||||
# macOS/Windows, which don't build .deb.
|
||||
USE_SYSTEM_FPM: "true"
|
||||
|
||||
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 200 KiB After Width: | Height: | Size: 25 KiB |
@@ -1,33 +1,40 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Ebene_2" data-name="Ebene 2" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 147.46 146.59">
|
||||
<!--
|
||||
Resenhacord app icon: the mark on the app's own night-blue ground (#0b0b10,
|
||||
the theme colour), inside the 22 %-radius rounded square the icon pipeline
|
||||
masks to. A soft violet glow behind the mark keeps the badge from reading as
|
||||
a flat black square at large sizes.
|
||||
-->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #1d1d1b;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: url(#Unbenannter_Verlauf_515);
|
||||
}
|
||||
|
||||
.cls-3 {
|
||||
fill: url(#Unbenannter_Verlauf_547);
|
||||
}
|
||||
</style>
|
||||
<linearGradient id="Unbenannter_Verlauf_515" data-name="Unbenannter Verlauf 515" x1="62.13" y1="77.74" x2="118.16" y2="77.74" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#fff"/>
|
||||
<stop offset=".95" stop-color="#9a86b8"/>
|
||||
<radialGradient id="glow" cx="512" cy="480" r="420" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#7c6cf6" stop-opacity="0.28"/>
|
||||
<stop offset="1" stop-color="#7c6cf6" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="back" x1="10" y1="12" x2="74" y2="74" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#c4b5fd"/>
|
||||
<stop offset="1" stop-color="#7c6cf6"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="Unbenannter_Verlauf_547" data-name="Unbenannter Verlauf 547" x1="9.21" y1="71.45" x2="135.41" y2="71.45" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#fff"/>
|
||||
<stop offset=".95" stop-color="#9a86b8"/>
|
||||
<linearGradient id="front" x1="42" y1="42" x2="94" y2="94" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#fda4af"/>
|
||||
<stop offset="1" stop-color="#f472b6"/>
|
||||
</linearGradient>
|
||||
<path id="frontBubble" d="M56 42 H80 A14 14 0 0 1 94 56 V68 A14 14 0 0 1 80 82 H76 L88 94 L68 82 H56 A14 14 0 0 1 42 68 V56 A14 14 0 0 1 56 42 Z"/>
|
||||
<mask id="gap" maskUnits="userSpaceOnUse" x="0" y="0" width="100" height="100">
|
||||
<rect width="100" height="100" fill="#fff"/>
|
||||
<use href="#frontBubble" fill="#000" stroke="#000" stroke-width="7" stroke-linejoin="round"/>
|
||||
</mask>
|
||||
</defs>
|
||||
<g id="Ebene_1-2" data-name="Ebene 1">
|
||||
<rect class="cls-1" x="0" y="0" width="147.46" height="146.59" rx="32.42" ry="32.42"/>
|
||||
<g>
|
||||
<path class="cls-2" d="M100.85,75.32c9.67-3.15,14.27-9.44,14.27-20.12,0-13.48-9.33-21.23-25.52-21.23h-16.13c-7.98,0-11.35,3.48-11.35,12.14v53.82h0c5.77,0,10.46-4.68,10.46-10.46v-7.44c-.06-.33-.1-.68-.09-1.05,0-3.35.07-6.69.12-10.04h-.03v-24.05c0-2.24,1.12-3.6,3.25-3.6h12.99c10.23,0,15.51,4.5,15.51,13.26s-5.62,14.38-15.29,14.38h-6.04c-.04,3.08-.1,6.14-.11,9.22h6.5c11.46,0,17.64,5.51,17.64,16.08s-8.01,16.22-19.36,16.22l-4.36.38h0l-10.57.41c-6.01,0-6.3,8.01-.3,8.25.34.01.68.02,1.03.02h17.37c17.32,0,27.31-8.99,27.31-24.28,0-12.14-5.84-19.56-17.3-21.92Z"/>
|
||||
<path class="cls-3" d="M102.43,119.43c-.64.25-1.29.48-1.93.7l-1.67.39c-.57.14-1.04.17-1.57.26-.51.09-1.02.16-1.51.21-.98.11-1.95.2-2.9.25-.96.07-1.88.12-2.82.16-3.72.14-7.36.14-10.98.07-1.81-.03-3.61-.1-5.41-.16l-2.67-.11c-.83-.03-1.76-.08-2.69-.13l-5.55-.32-.63-.04-.81-.09c-.54-.08-1.08-.16-1.62-.27-1.07-.22-2.13-.5-3.17-.88-2.07-.74-4.05-1.78-5.83-3.11-.9-.66-1.74-1.39-2.53-2.18l-1.91-1.98-7.37-7.7-7.37-7.7c-2.49-2.54-4.81-5.23-7.56-7.52-2.75-2.29-5.15-4.91-7.6-7.49-1.23-1.29-2.4-2.62-3.61-3.93l-.45-.49-.22-.25-.34-.38c-.59-.67-1.07-1.43-1.47-2.23-.77-1.61-1.13-3.41-.99-5.18.11-1.76.72-3.49,1.7-4.96.26-.35.51-.72.81-1.04l.45-.48.25-.25.96-.93,3.83-3.71,7.66-7.42,15.32-14.84,7.66-7.42.99-.95c.4-.38.8-.74,1.23-1.09.85-.69,1.73-1.33,2.67-1.87,1.87-1.11,3.91-1.92,6.02-2.42,1.06-.24,2.13-.4,3.2-.49.54-.04,1.08-.06,1.62-.06h.73s.67.02.67.02l5.33.11,10.66.21,21.32.42,10.72.23c4.1.16,8.16,1.58,11.38,4.07.83.6,1.56,1.31,2.29,2.01.67.76,1.36,1.51,1.93,2.35,1.16,1.65,2.12,3.46,2.7,5.4.59,1.92.97,3.93.94,5.93,0,1.87-.06,3.58-.09,5.38l-.85,42.64-.22,10.68c-.12,3.6-.47,7.31-1.88,10.77-.69,1.72-1.64,3.4-2.98,4.74-1.32,1.36-2.95,2.38-4.68,3.05-1.75.64-3.58.92-5.39,1.06-.9.02-1.82.04-2.71.02-.44-.01-.89,0-1.33-.06-.22-.02-.44-.06-.67-.1l-.33-.07c-.11-.03-.22.01-.33-.26.11-.27.22-.22.33-.26l.33-.07c.22-.04.44-.08.67-.1.44-.05.89-.04,1.33-.06.89-.02,1.75-.07,2.62-.12,1.72-.2,3.42-.54,5-1.18,1.55-.67,2.95-1.64,4.06-2.88,1.13-1.22,1.92-2.74,2.48-4.33,1.16-3.19,1.37-6.71,1.34-10.18l-.2-10.64-.85-42.65c-.04-1.76-.05-3.61-.12-5.28-.04-1.56-.4-3.08-.9-4.52-.5-1.46-1.29-2.79-2.21-4.01-.45-.62-1-1.16-1.52-1.71-.58-.5-1.14-1.02-1.78-1.43-2.48-1.76-5.49-2.67-8.48-2.67l-10.6.19-21.32.42-10.66.21-5.33.11h-.67c-.23.02-.45.02-.6.03-.35,0-.7.04-1.04.08-.69.09-1.38.22-2.05.4-1.34.38-2.61.95-3.77,1.7-.58.37-1.12.8-1.64,1.25-.26.23-.5.47-.74.71l-.9.94-7.36,7.72-14.72,15.43-7.36,7.72-3.68,3.86-.92.96-.22.23s0,.01,0,.01c-.01,0,.01.02,0,.01.02.02.02.02.05.04,0,.01.05,0,.04,0,.02-.02,0-.01.02-.03l.81.83c1.24,1.28,2.5,2.53,3.71,3.82,2.43,2.59,4.91,5.14,7.04,8.02,2.13,2.88,4.69,5.35,7.08,7.98l7.27,7.8,7.27,7.8,1.76,1.89c.46.48.95.92,1.48,1.32,1.06.8,2.27,1.43,3.54,1.88.64.23,1.3.4,1.97.54.33.07.67.12,1.01.17l.51.06c.33.03.46.03.7.04l5.09.27c.84.05,1.69.1,2.63.12l2.65.1c1.76.06,3.51.12,5.25.15,3.48.06,6.96.07,10.34-.17,1.68-.1,3.36-.28,4.9-.57.4-.06.77-.14,1.12-.23.35-.09.76-.16,1.04-.27l.9-.31c.18-.11.36-.22.54-.3l.65-.29c2.43-1.07,5.26.03,6.33,2.46,1.07,2.43-.03,5.26-2.46,6.33-.05.02-.12.05-.17.07Z"/>
|
||||
<rect width="1024" height="1024" rx="225" fill="#0b0b10"/>
|
||||
<rect width="1024" height="1024" rx="225" fill="url(#glow)"/>
|
||||
<g transform="translate(192 192) scale(6.4)">
|
||||
<path mask="url(#gap)" fill="url(#back)"
|
||||
d="M26 12 H58 A16 16 0 0 1 74 28 V44 A16 16 0 0 1 58 60 H34 L16 74 L20 60 H26 A16 16 0 0 1 10 44 V28 A16 16 0 0 1 26 12 Z"/>
|
||||
<use href="#frontBubble" fill="url(#front)"/>
|
||||
<g fill="#fff">
|
||||
<circle cx="58" cy="62" r="3.6"/>
|
||||
<circle cx="68" cy="62" r="3.6"/>
|
||||
<circle cx="78" cy="62" r="3.6"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 2.0 KiB |
@@ -1,14 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Ebene_2" data-name="Ebene 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 123.08 100.14">
|
||||
<!-- Monochrome silhouette of mark.svg for macOS menu-bar templates (alpha only). -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #1d1d1b;
|
||||
}
|
||||
</style>
|
||||
<path id="frontBubble" d="M56 42 H80 A14 14 0 0 1 94 56 V68 A14 14 0 0 1 80 82 H76 L88 94 L68 82 H56 A14 14 0 0 1 42 68 V56 A14 14 0 0 1 56 42 Z"/>
|
||||
<mask id="gap">
|
||||
<rect width="100" height="100" fill="#fff"/>
|
||||
<use href="#frontBubble" fill="#000" stroke="#000" stroke-width="7" stroke-linejoin="round"/>
|
||||
</mask>
|
||||
<mask id="dots">
|
||||
<rect width="100" height="100" fill="#fff"/>
|
||||
<circle cx="58" cy="62" r="3.6" fill="#000"/>
|
||||
<circle cx="68" cy="62" r="3.6" fill="#000"/>
|
||||
<circle cx="78" cy="62" r="3.6" fill="#000"/>
|
||||
</mask>
|
||||
</defs>
|
||||
<g id="Ebene_1-2" data-name="Ebene 1">
|
||||
<path class="cls-1" d="M91.64,53.93c9.67-3.15,14.27-9.44,14.27-20.12,0-13.48-9.33-21.23-25.52-21.23h-16.13c-7.98,0-11.35,3.48-11.35,12.14v53.82h0c5.77,0,10.46-4.68,10.46-10.46v-7.44c-.06-.33-.1-.68-.09-1.05,0-3.35.07-6.69.12-10.04h-.03v-24.05c0-2.24,1.12-3.6,3.25-3.6h12.99c10.23,0,15.51,4.5,15.51,13.26s-5.62,14.38-15.29,14.38h-6.04c-.04,3.08-.1,6.14-.11,9.22h6.5c11.46,0,17.64,5.51,17.64,16.08s-8.01,16.22-19.36,16.22l-4.36.38h0l-10.57.41c-6.01,0-6.3,8.01-.3,8.25.34.01.68.02,1.03.02h17.37c17.32,0,27.31-8.99,27.31-24.28,0-12.14-5.84-19.56-17.3-21.92Z"/>
|
||||
<path class="cls-1" d="M93.21,98.04c-1.58.67-3.48,1.14-5.16,1.36-7.86,1.04-15.77.76-23.61.52-3.06-.04-7.8-.42-10.92-.56-2.12-.13-4.23-.56-6.23-1.28-4.01-1.34-7.48-4.13-10.26-7.27-3.66-3.85-11.08-11.56-14.75-15.4-2.49-2.54-4.81-5.23-7.56-7.52-4.08-3.48-7.59-7.49-11.21-11.42-3.78-3.58-4.71-9.08-1.77-13.49.6-.98,1.66-1.88,2.47-2.69,6.89-6.67,19.79-19.17,26.81-25.96,0,0,7.66-7.42,7.66-7.42,1.47-1.45,3.08-2.88,4.89-3.91C46.83,1.04,50.61,0,54.4,0c.89-.03,5.73.13,6.73.12,0,0,10.66.21,10.66.21,7.02.15,25.23.49,32.04.65,8.24.22,15.88,5.97,18.3,13.83,1.26,3.64.92,7.57.85,11.31,0,0-.85,42.64-.85,42.64-.31,13.92,1.41,31-17.86,30.33-.78,0-1.57-.04-2.33-.23-.11-.03-.22.01-.33-.26.11-.27.22-.22.33-.26,3.35-.48,6.76-.1,9.95-1.53,6.81-2.78,8.04-10.78,7.88-17.39-.14-6.81-.88-45.63-1.05-53.3-.12-3.34.17-6.68-1.03-9.81-1.03-2.88-3-5.41-5.51-7.15-2.48-1.76-5.49-2.67-8.48-2.67,0,0-10.6.19-10.6.19-9.89.2-27.61.55-37.32.74-3.9-.11-7.81,1.33-10.51,4.18-.22.33-7.8,8.1-8.26,8.65-4.86,5.11-17.37,18.21-22.08,23.15-.73.81-4.11,4.27-4.82,5.05,0,0,0,.01,0,.01-.01,0,.01.02,0,.01.02.02.08.07.09.03.02-.02,0-.01.02-.03,0,0,.81.83.81.83,3.71,3.84,7.52,7.57,10.75,11.84,3.79,4.86,10.17,11.13,14.34,15.78,0,0,7.27,7.8,7.27,7.8,1.85,2.11,4.03,4.24,6.79,5.09,2.44.99,6.67.88,9.28,1.08,3.39.17,7.13.31,10.53.37,5.86.03,11.79.22,17.4-1.24,0,0,.9-.31.9-.31.35-.2.82-.44,1.2-.58,6.01-2.4,9.65,6.26,3.7,8.87h0Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
<path mask="url(#gap)" fill="#1d1d1b"
|
||||
d="M26 12 H58 A16 16 0 0 1 74 28 V44 A16 16 0 0 1 58 60 H34 L16 74 L20 60 H26 A16 16 0 0 1 10 44 V28 A16 16 0 0 1 26 12 Z"/>
|
||||
<use href="#frontBubble" fill="#1d1d1b" mask="url(#dots)"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 1.0 KiB |
@@ -1,28 +1,33 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Ebene_2" data-name="Ebene 2" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 123.07 100.13">
|
||||
<!--
|
||||
Resenhacord mark: two speech bubbles in conversation, the front one talking.
|
||||
Square viewBox so every raster target (tray, favicon, PWA) scales without
|
||||
letterboxing. The gap between the bubbles is cut with a mask rather than
|
||||
painted, so the mark stays legible on any background and the macOS template
|
||||
(alpha-only) variant keeps the same silhouette.
|
||||
-->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: url(#Unbenannter_Verlauf_515);
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: url(#Unbenannter_Verlauf_547);
|
||||
}
|
||||
</style>
|
||||
<linearGradient id="Unbenannter_Verlauf_515" data-name="Unbenannter Verlauf 515" x1="52.91" y1="56.35" x2="108.94" y2="56.35" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#fff"/>
|
||||
<stop offset=".95" stop-color="#9a86b8"/>
|
||||
<linearGradient id="back" x1="10" y1="12" x2="74" y2="74" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#c4b5fd"/>
|
||||
<stop offset="1" stop-color="#7c6cf6"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="Unbenannter_Verlauf_547" data-name="Unbenannter Verlauf 547" x1="0" y1="50.06" x2="126.19" y2="50.06" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#fff"/>
|
||||
<stop offset=".95" stop-color="#9a86b8"/>
|
||||
<linearGradient id="front" x1="42" y1="42" x2="94" y2="94" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#fda4af"/>
|
||||
<stop offset="1" stop-color="#f472b6"/>
|
||||
</linearGradient>
|
||||
<path id="frontBubble" d="M56 42 H80 A14 14 0 0 1 94 56 V68 A14 14 0 0 1 80 82 H76 L88 94 L68 82 H56 A14 14 0 0 1 42 68 V56 A14 14 0 0 1 56 42 Z"/>
|
||||
<mask id="gap">
|
||||
<rect width="100" height="100" fill="#fff"/>
|
||||
<use href="#frontBubble" fill="#000" stroke="#000" stroke-width="7" stroke-linejoin="round"/>
|
||||
</mask>
|
||||
</defs>
|
||||
<g id="Ebene_1-2" data-name="Ebene 1">
|
||||
<g>
|
||||
<path class="cls-1" d="M91.64,53.93c9.67-3.15,14.27-9.44,14.27-20.12,0-13.48-9.33-21.23-25.52-21.23h-16.13c-7.98,0-11.35,3.48-11.35,12.14v53.82h0c5.77,0,10.46-4.68,10.46-10.46v-7.44c-.06-.33-.1-.68-.09-1.05,0-3.35.07-6.69.12-10.04h-.03v-24.05c0-2.24,1.12-3.6,3.25-3.6h12.99c10.23,0,15.51,4.5,15.51,13.26s-5.62,14.38-15.29,14.38h-6.04c-.04,3.08-.1,6.14-.11,9.22h6.5c11.46,0,17.64,5.51,17.64,16.08s-8.01,16.22-19.36,16.22l-4.36.38h0l-10.57.41c-6.01,0-6.3,8.01-.3,8.25.34.01.68.02,1.03.02h17.37c17.32,0,27.31-8.99,27.31-24.28,0-12.14-5.84-19.56-17.3-21.92Z"/>
|
||||
<path class="cls-2" d="M93.21,98.04c-.64.25-1.29.48-1.93.7l-1.67.39c-.57.14-1.04.17-1.57.26-.51.09-1.02.16-1.51.21-.98.11-1.95.2-2.9.25-.96.07-1.88.12-2.82.16-3.72.14-7.36.14-10.98.07-1.81-.03-3.61-.1-5.41-.16l-2.67-.11c-.83-.03-1.76-.08-2.69-.13l-5.55-.32-.63-.04-.81-.09c-.54-.08-1.08-.16-1.62-.27-1.07-.22-2.13-.5-3.17-.88-2.07-.74-4.05-1.78-5.83-3.11-.9-.66-1.74-1.39-2.53-2.18l-1.91-1.98-7.37-7.7-7.37-7.7c-2.49-2.54-4.81-5.23-7.56-7.52-2.75-2.29-5.15-4.91-7.6-7.49-1.23-1.29-2.4-2.62-3.61-3.93l-.45-.49-.22-.25-.34-.38c-.59-.67-1.07-1.43-1.47-2.23-.77-1.61-1.13-3.41-.99-5.18.11-1.76.72-3.49,1.7-4.96.26-.35.51-.72.81-1.04l.45-.48.25-.25.96-.93,3.83-3.71,7.66-7.42,15.32-14.84,7.66-7.42.99-.95c.4-.38.8-.74,1.23-1.09.85-.69,1.73-1.33,2.67-1.87,1.87-1.11,3.91-1.92,6.02-2.42,1.06-.24,2.13-.4,3.2-.49.54-.04,1.08-.06,1.62-.06h.73s.67.02.67.02l5.33.11,10.66.21,21.32.42,10.72.23c4.1.16,8.16,1.58,11.38,4.07.83.6,1.56,1.31,2.29,2.01.67.76,1.36,1.51,1.93,2.35,1.16,1.65,2.12,3.46,2.7,5.4.59,1.92.97,3.93.94,5.93,0,1.87-.06,3.58-.09,5.38l-.85,42.64-.22,10.68c-.12,3.6-.47,7.31-1.88,10.77-.69,1.72-1.64,3.4-2.98,4.74-1.32,1.36-2.95,2.38-4.68,3.05-1.75.64-3.58.92-5.39,1.06-.9.02-1.82.04-2.71.02-.44-.01-.89,0-1.33-.06-.22-.02-.44-.06-.67-.1l-.33-.07c-.11-.03-.22.01-.33-.26.11-.27.22-.22.33-.26l.33-.07c.22-.04.44-.08.67-.1.44-.05.89-.04,1.33-.06.89-.02,1.75-.07,2.62-.12,1.72-.2,3.42-.54,5-1.18,1.55-.67,2.95-1.64,4.06-2.88,1.13-1.22,1.92-2.74,2.48-4.33,1.16-3.19,1.37-6.71,1.34-10.18l-.2-10.64-.85-42.65c-.04-1.76-.05-3.61-.12-5.28-.04-1.56-.4-3.08-.9-4.52-.5-1.46-1.29-2.79-2.21-4.01-.45-.62-1-1.16-1.52-1.71-.58-.5-1.14-1.02-1.78-1.43-2.48-1.76-5.49-2.67-8.48-2.67l-10.6.19-21.32.42-10.66.21-5.33.11h-.67c-.23.02-.45.02-.6.03-.35,0-.7.04-1.04.08-.69.09-1.38.22-2.05.4-1.34.38-2.61.95-3.77,1.7-.58.37-1.12.8-1.64,1.25-.26.23-.5.47-.74.71l-.9.94-7.36,7.72-14.72,15.43-7.36,7.72-3.68,3.86-.92.96-.22.23s0,.01,0,.01c-.01,0,.01.02,0,.01.02.02.02.02.05.04,0,.01.05,0,.04,0,.02-.02,0-.01.02-.03l.81.83c1.24,1.28,2.5,2.53,3.71,3.82,2.43,2.59,4.91,5.14,7.04,8.02,2.13,2.88,4.69,5.35,7.08,7.98l7.27,7.8,7.27,7.8,1.76,1.89c.46.48.95.92,1.48,1.32,1.06.8,2.27,1.43,3.54,1.88.64.23,1.3.4,1.97.54.33.07.67.12,1.01.17l.51.06c.33.03.46.03.7.04l5.09.27c.84.05,1.69.1,2.63.12l2.65.1c1.76.06,3.51.12,5.25.15,3.48.06,6.96.07,10.34-.17,1.68-.1,3.36-.28,4.9-.57.4-.06.77-.14,1.12-.23.35-.09.76-.16,1.04-.27l.9-.31c.18-.11.36-.22.54-.3l.65-.29c2.43-1.07,5.26.03,6.33,2.46,1.07,2.43-.03,5.26-2.46,6.33-.05.02-.12.05-.17.07Z"/>
|
||||
</g>
|
||||
<path mask="url(#gap)" fill="url(#back)"
|
||||
d="M26 12 H58 A16 16 0 0 1 74 28 V44 A16 16 0 0 1 58 60 H34 L16 74 L20 60 H26 A16 16 0 0 1 10 44 V28 A16 16 0 0 1 26 12 Z"/>
|
||||
<use href="#frontBubble" fill="url(#front)"/>
|
||||
<g fill="#fff">
|
||||
<circle cx="58" cy="62" r="3.6"/>
|
||||
<circle cx="68" cy="62" r="3.6"/>
|
||||
<circle cx="78" cy="62" r="3.6"/>
|
||||
</g>
|
||||
</svg>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,56 @@
|
||||
# Gerar o instalador do app desktop
|
||||
|
||||
O workflow **`.github/workflows/release.yml`** (herdado do upstream) já compila
|
||||
para macOS, Windows e Linux e publica os instaladores como release. Ele dispara
|
||||
ao empurrar uma tag `v*`.
|
||||
|
||||
Não foi preciso escrever workflow novo — foi preciso **adaptá-lo ao fork**.
|
||||
|
||||
## Por que GitHub e não Gitea
|
||||
|
||||
O Gitea desta instância não tem Actions habilitado, e mesmo habilitado ele
|
||||
**não oferece máquinas hospedadas**: seria preciso registrar um PC Windows
|
||||
como runner e mantê-lo ligado. O GitHub fornece runners Windows prontos, que é
|
||||
exatamente o que falta — o módulo nativo do compartilhamento de áudio usa
|
||||
WASAPI e só compila no Windows, com o compilador da Microsoft.
|
||||
|
||||
O Gitea continua sendo o repositório principal. O GitHub entra apenas como
|
||||
espelho para compilar.
|
||||
|
||||
## Passos (uma vez)
|
||||
|
||||
1. Criar um repositório no GitHub — o `electron-builder.yml` está apontado para
|
||||
`syncwrld/resenhacord`. **Se o seu for outro nome, ajuste lá.**
|
||||
2. Adicionar o espelho e empurrar:
|
||||
```
|
||||
cd /opt/backspace
|
||||
git remote add github git@github.com:syncwrld/resenhacord.git
|
||||
git push github main
|
||||
```
|
||||
3. Marcar uma versão e empurrar a tag — é ela que dispara a compilação:
|
||||
```
|
||||
git tag v1.0.1 && git push github v1.0.1
|
||||
```
|
||||
4. Os instaladores aparecem na aba Releases do GitHub em ~15 minutos.
|
||||
|
||||
## O que isso resolve de quebra
|
||||
|
||||
O `electron-updater` já estava instalado no projeto mas **sem feed** — o app
|
||||
não se atualizava sozinho. Como o `publish` agora aponta para as releases do
|
||||
fork, o app passa a encontrar versões novas por conta própria. Ninguém do grupo
|
||||
precisa reinstalar na mão de novo.
|
||||
|
||||
## Adaptações feitas para o fork
|
||||
|
||||
| O quê | Por quê |
|
||||
|---|---|
|
||||
| `publish.owner` → `syncwrld` | Apontava para o repositório do upstream |
|
||||
| `postinstall` reconstrói também `electron-native-screenshare` | Rodava `electron-rebuild` só no `uiohook-napi`. O módulo novo seria compilado para a ABI do Node em vez da do Electron e falharia ao carregar — e como o carregamento degrada em silêncio, o sintoma seria "compartilha sem som", sem erro visível |
|
||||
| Nota sobre `asarUnpack` | O `uiohook-napi` tem `build/` excluído do asar porque distribui binários prontos. O módulo novo **não** distribui: `build/Release/*.node` é a única cópia e não pode ser excluída |
|
||||
|
||||
## Detalhe herdado que vale preservar
|
||||
|
||||
O runner do Windows está fixado em `windows-2022`, não `windows-latest`. O
|
||||
comentário no workflow explica: a imagem `latest` traz o Visual Studio 18, que
|
||||
o node-gyp embutido no `electron-rebuild` não detecta ("unknown version
|
||||
undefined"). Não mude isso sem testar.
|
||||
@@ -21,11 +21,29 @@ código e os commits seguem em inglês, como o resto do repositório.
|
||||
| Spotify por OAuth (atividade com capa e progresso) | `75316b08` | Token só no servidor; `state` assinado; atividades por fonte para não brigar com o detector do Electron |
|
||||
| Menu do próprio nome | `f5451e1b` | Perfil, status e copiar ID. Sem Clips/trocar conta — não existem aqui |
|
||||
| Cronômetro da call | `f5451e1b` | `startedAt` do servidor; zera sozinho porque sala vazia é destruída |
|
||||
| Correções do Spotify (sincronia, sumiço, barra) | `c899253e` | Pausa virou estado; horário do servidor no `ready` corrige relógio |
|
||||
| Soundboard no app desktop | `c899253e` | `window.prompt` não existe no Electron; campo inline no lugar |
|
||||
| Áudio do sistema sem eco | `d525bbb8` `5b848432` | Módulo nativo com isolamento por processo + ganho e preset de música |
|
||||
| Instaladores publicados pelo Gitea | `ce6bba05` | GitHub compila, Gitea distribui; atualização automática ligada |
|
||||
| Fixar mensagens no canal | `4ceb5cd6` | Estado na própria mensagem; painel no cabeçalho; limite de 50 |
|
||||
| Busca: filtros embutidos e tradução | `50a8f12c` | Os filtros já existiam; faltava `de:fulano` e descoberta |
|
||||
| Emojis e figurinhas do servidor | `7d003021` | `:nome:` no markdown sem plugin; figurinha como mensagem inteira |
|
||||
| Fundação de i18n (en + pt-BR) | `688a1335` | `src/i18n/`; pt-BR é parcial e cai para inglês. Aba Idioma nas configurações |
|
||||
| Rebrand: **Resenhacord** + logo nova | (ver git log) | `productName` mudou, `appId` **não** — assim o NSIS troca a instalação antiga no lugar. `userData` migra `Backspace` → `Resenhacord` no primeiro boot (senão todo mundo cairia da sessão). Logo: duas bolhas de conversa, roxo → rosa; fonte em `assets/brand/*.svg`, PNGs e ícones gerados por `pnpm gen-icons` |
|
||||
| Aviso "X fixou uma mensagem" | (ver git log) | Tabela `messages` ganhou `type` (migração 0018); a rota de pin posta uma mensagem de sistema. Dois links: pular pra mensagem e abrir o painel de fixadas |
|
||||
| Menção `@everyone` | (ver git log) | Reusa o token `<@id>`; permissão `MENTION_EVERYONE` nova, concedida ao `@everyone` por padrão e retroativa via backfill no boot. Servidor neutraliza o token de quem não tem a permissão, no envio **e** na edição |
|
||||
| Renomear canal (nome e tópico) | (ver git log) | Rota `PATCH /api/channels/:id` já existia; faltava interface. Normalização em `shared/constants` para servidor e web usarem a mesma regra |
|
||||
| Volume da tela em 50% por padrão | (ver git log) | `DEFAULT_STREAM_VOLUME`; o publicador reforça o áudio do sistema, então o padrão anterior chegava alto demais |
|
||||
| Sons 50% mais baixos | (ver git log) | Só `SFX_BASE_VOLUME` (0.8 → 0.4); os `.ogg` não foram regerados |
|
||||
| Atualização do desktop verificada com mais frequência | (ver git log) | Era só na abertura e de 4 em 4 horas; agora a cada 30 min e ao focar a janela, com trava de 5 min |
|
||||
| Preview de perfil nos participantes da call | (ver git log) | O popout já existia e era aberto de 11 lugares; **nenhum era de voz**. Ligado nas linhas da lista de voz e no nome dos tiles da grade |
|
||||
|
||||
## Já existia no código (verificado, não construir de novo)
|
||||
|
||||
- **Busca com filtros** — servidor (`q`, `from`, `has`, `before`, `after`),
|
||||
client de API e painel no `SearchPopover` já existiam completos. Só faltava
|
||||
tradução, sintaxe embutida (`de:fulano`) e descoberta.
|
||||
|
||||
- **Animação de digitação** — `TypingIndicator.tsx`, três pontos `animate-bounce`
|
||||
escalonados em 0/150/300ms.
|
||||
- **Sons de call/stream** — `SoundController.tsx`, montado no `AppLayout`:
|
||||
@@ -47,12 +65,33 @@ Ordem sugerida: as pequenas primeiro, as grandes uma de cada vez.
|
||||
| # | Feature | Tamanho | Observação técnica |
|
||||
|---|---|---|---|
|
||||
|
||||
## Identidade do perfil (entregue 2026-09-02, pedido: "completo e FODA")
|
||||
|
||||
Uma coluna JSON (`users.profile_style`, migração 0019) normalizada no servidor
|
||||
por `shared/profile.ts`. Aba **Perfil** nas configurações com prévia ao vivo
|
||||
(`ProfilePanel` renderiza o mesmo `ProfileCard` que o popout).
|
||||
|
||||
| Peça | Onde aparece | Detalhe |
|
||||
|---|---|---|
|
||||
| Estilo do nome — 10 fontes × 9 efeitos × 2 cores | popout, perfil completo | Fontes OFL em `public/fonts`; efeitos são CSS (`.name-fx-*`) |
|
||||
| Decoração do avatar — 10 | popout, perfil, editor | SVG inline em `DecoratedAvatar`, sem imagens |
|
||||
| Efeito do perfil — 9 | banner do popout e do perfil | Partículas CSS com posições semeadas (`ProfileEffectLayer`) |
|
||||
| Nameplate — 10 + personalizado | lista de membros | `.nameplate-*` |
|
||||
| Tema (2 cores) | fundo do cartão e banner sem imagem | |
|
||||
| Pronomes, tag (emoji + 2–4 letras) | ao lado do nome; tag também na lista de membros | |
|
||||
| Links (até 5) | popout e aba Sobre | Ícone detectado pelo host; só https |
|
||||
| Quadro: jogo favorito, rotação (5), quero jogar (10), citação | aba Quadro do perfil; tira "Coleção" no popout | Busca na Steam via `/api/games/search` (proxy, cache 10 min); capas só de CDN da Steam |
|
||||
| Emblemas derivados | popout e perfil | admin, dono, fundador (1º mês), veterano (1 ano), OG (6 meses), estiloso — nada é armazenado |
|
||||
|
||||
Fora do escopo desta entrega, candidatos a seguir: perfil por servidor
|
||||
(apelido/avatar por servidor), emblemas concedidos pelo admin, molduras
|
||||
animadas por upload, status com expiração.
|
||||
|
||||
## Pendente — ideias aprovadas
|
||||
|
||||
| Feature | Tamanho | Observação técnica |
|
||||
|---|---|---|
|
||||
| Watch party | Grande | O screen share do LiveKit já existe; falta sincronizar posição de reprodução entre participantes |
|
||||
| Emojis e stickers do grupo | Média | `UPLOAD_DIR` e o pipeline de upload já existem; falta tabela por espaço e resolução no render de mensagem |
|
||||
|
||||
## Regra de idioma (a partir de 2026-08-31)
|
||||
|
||||
@@ -72,21 +111,52 @@ fixas por `t('chave')`. O que faltar cai para o inglês sozinho.
|
||||
- Configurações → Privacidade
|
||||
- Configurações → Conexões (nasceu bilíngue)
|
||||
- Seletor de GIF
|
||||
- Chat: composer e mensagens (placeholder, anexos, responder, reações)
|
||||
- Barra lateral: servidores, canais e menus de contexto
|
||||
- Lista de membros (grupos por cargo, carregamento)
|
||||
- Busca (rótulos, filtros e sintaxe embutida)
|
||||
- Registro de auditoria e Estatísticas (nasceram bilíngues)
|
||||
- Configurações do canal (visão geral, privacidade, exclusão e rótulos de permissão)
|
||||
- Amigos: abas, listas, pedidos, aba de adicionar e cartões de descoberta
|
||||
- Modal de convite (incluindo os motivos de falha por convite)
|
||||
- Lista de membros da DM em grupo (remover e transferir propriedade)
|
||||
- Configurações → Área de trabalho (início automático, atualizações, instância)
|
||||
- Configurações → Voz: dispositivo de saída (completa a aba, junto com a entrada)
|
||||
- Configurações → Atalhos (incluindo os nomes das ações e o aviso de conflito)
|
||||
- Configurações → Conta (perfil, senha, conta desligada da origem, zona de perigo)
|
||||
- Explorar servidores (cartões, entrar, pedir para entrar)
|
||||
- Configurações → Voz: vídeo (prévia, permissões, escolha de câmera) — fecha a aba
|
||||
- Criar canal, categoria e servidor
|
||||
- DMs em grupo (nova DM, adicionar amigos, configurações do grupo, sair)
|
||||
- Entrar num servidor (descoberta, código de convite, conectar e login federado)
|
||||
- Excluir conta, transferir propriedade e a tela de erro geral
|
||||
- Configurações do servidor → Visão geral (identidade, ícone, banner, zona de perigo)
|
||||
- Configurações do servidor → Membros e banimentos
|
||||
- Configurações do servidor → Cargos (incluindo a tabela de permissões)
|
||||
- Configurações da categoria (renomear, privacidade, exceções, excluir)
|
||||
- Configurações da instância → Geral (nome, descoberta, busca de GIFs)
|
||||
- Mensagens de sistema do chat e a prévia delas na barra lateral
|
||||
- Login, registro e a página de convite
|
||||
- Voz: controles, informações de conexão, folha de entrada no celular, aba Voz
|
||||
- Telas do celular (`Mobile*.tsx`)
|
||||
- Navegação das configurações (servidor, usuário e instância) + Descoberta e
|
||||
pedidos de entrada
|
||||
|
||||
Os rótulos de permissão viraram um conjunto único (`perm.*`), usado pelo painel
|
||||
de cargos e pelos editores de exceção de canal e de categoria — antes havia
|
||||
duas cópias da mesma lista.
|
||||
|
||||
### Fila sugerida de tradução
|
||||
|
||||
Mensagens e composer · lista de membros · servidores e canais · amigos e DMs ·
|
||||
modais de convite · configurações restantes · telas de erro
|
||||
Configurações → Instância: registro, federação, transmissão, armazenamento e
|
||||
usuários (os cinco painéis grandes, só o dono vê) · `ConnectedInstances` ·
|
||||
`UserProfileModal` · `MainContent` e `ChannelSidebar` · mensagens de erro
|
||||
vindas de stores e hooks (`instanceStore`, `useLiveKit`, `transferStore`)
|
||||
|
||||
## Correções pendentes
|
||||
|
||||
| Problema | Causa provável | Correção |
|
||||
|---|---|---|
|
||||
| **Enviar som ao soundboard não funciona no app desktop** (funciona no navegador) | `SoundboardPopover` pede o nome do som com `window.prompt`, que o **Electron não implementa** — não abre nada e devolve vazio, então o fluxo aborta em silêncio, sem erro. `window.prompt` aparece em exatamente um lugar no projeto: esse. O resto do código já o evitava | Trocar por um campo de texto dentro do próprio popover (ou um modal reutilizável). Some o prompt e passa a funcionar igual nos dois. **Vale criar o modal de entrada genérico**, já que não existe nenhum e outras features vão precisar |
|
||||
| **Bloco do Spotify dessincronizado** (mostra a faixa errada por um tempo) | Soma de duas esperas: a consulta roda a cada **20s** (`useSpotifyActivity`) e o envio pelo WebSocket ainda passa por um **debounce de 5s** no `activityStore`. Na pior hipótese os outros veem a música anterior por ~25s | Consultar de novo perto do fim da faixa (a duração é conhecida) em vez de só por intervalo fixo, e encurtar o debounce para esta fonte |
|
||||
| **Bloco do Spotify some sozinho** | Três caminhos apagam a atividade inteira: faixa **pausada** (`is_playing: false` devolve `null`), o **vão entre faixas** (o Spotify responde `204`) e qualquer falha transitória. Some e volta = a piscada que você viu | Manter a última faixa conhecida por alguns segundos antes de apagar, e enviar um estado *pausado* explícito em vez de sumir com o bloco |
|
||||
| **Barra de progresso errada / andando pausada** | Duas causas independentes: (1) o progresso é derivado de carimbos calculados com o relógio do **servidor** e desenhado contra o relógio de **quem olha** — se os relógios divergem, a barra fica deslocada; (2) a barra continua avançando localmente depois que a pessoa pausa, até a próxima consulta | Enviar o horário do servidor junto no payload para o cliente corrigir a diferença, e congelar a barra quando o estado for pausado |
|
||||
|
||||
|
||||
## App desktop — já existia (verificado 2026-08-31)
|
||||
@@ -111,8 +181,6 @@ Escolhidas pelo dono, inspiradas no Discord. **Nenhuma iniciada.**
|
||||
|
||||
| Feature | Tamanho | Observação técnica |
|
||||
|---|---|---|
|
||||
| **Fixar mensagens no canal** | Pequena | Tabela de fixadas por canal + rota de listagem; permissão natural é `MANAGE_MESSAGES` |
|
||||
| **Busca com filtros** | Média | `SearchPopover` já existe; falta filtrar por autor, canal e presença de anexo — e índice no banco, senão fica lento quando o histórico crescer |
|
||||
| **Resposta rápida de voz (soundboard por atalho)** | Pequena | O soundboard já está pronto; falta só marcar favoritos e ligar a atalhos de teclado, sem abrir painel. Cuidado: o limite de repetição do servidor continua valendo e é o que impede virar metralhadora |
|
||||
| **Cargos com ícone e exibição separada** | Média | Cargos e cores já existem; falta ícone, o campo "exibir separadamente" e o agrupamento na lista de membros |
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# Scripts de operação da instância
|
||||
|
||||
Rodam na VPS, em `/opt/ops`, onde os arquivos são links simbólicos para esta
|
||||
pasta — o repositório é a única cópia de verdade. Antes disso eles existiam só
|
||||
na VPS, sem versão nem backup: uma reinstalação do servidor levaria junto todo
|
||||
o processo de publicação.
|
||||
|
||||
| script | o que faz |
|
||||
| --- | --- |
|
||||
| `call-check.sh` | Diz se tem alguém em call. Roda antes de reiniciar o servidor. |
|
||||
| `release-desktop.sh` | Espelha o fork no GitHub e dispara a compilação dos instaladores de desktop. |
|
||||
| `release-android.sh` | Compila o APK no GitHub, assina na VPS e publica no Gitea. |
|
||||
|
||||
## O que estes scripts leem e não está aqui
|
||||
|
||||
Nada de segredo entra no repositório. Os arquivos abaixo ficam só na VPS, com
|
||||
permissão `600`:
|
||||
|
||||
- `/root/.github-token` — dispara e baixa os builds do GitHub.
|
||||
- `/root/.gitea-token` — publica os anexos das releases.
|
||||
- `/root/android-signing/` — a chave que assina o APK, mais a senha dela.
|
||||
|
||||
A chave do Android merece atenção especial: ela é o que prova ao Android que
|
||||
uma atualização veio de nós. Se for perdida, ninguém consegue atualizar por
|
||||
cima — todo mundo tem que desinstalar e instalar de novo, e não existe
|
||||
recuperação. Mantenha uma cópia fora da VPS.
|
||||
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env bash
|
||||
# Reports how many people are currently in a call on this instance.
|
||||
#
|
||||
# Asks LiveKit directly (ListRooms over its twirp API) rather than inferring
|
||||
# from the app: LiveKit is the authority on who is actually connected to media.
|
||||
# Prints the participant count and exits 0 when the instance is idle, 1 when
|
||||
# someone is in a call — so it can gate a deploy.
|
||||
set -euo pipefail
|
||||
|
||||
ENV_FILE=${ENV_FILE:-/opt/backspace/.env}
|
||||
# `|| true`: under `set -o pipefail` a grep with no match aborts the script
|
||||
# before the check below, exiting 1 — which in this script's contract means
|
||||
# "someone is in a call" rather than "I could not tell".
|
||||
KEY=$(grep -E '^LIVEKIT_API_KEY=' "$ENV_FILE" 2>/dev/null | cut -d= -f2- || true)
|
||||
SECRET=$(grep -E '^LIVEKIT_API_SECRET=' "$ENV_FILE" 2>/dev/null | cut -d= -f2- || true)
|
||||
[ -n "$KEY" ] && [ -n "$SECRET" ] || { echo "LiveKit credentials not found in $ENV_FILE" >&2; exit 2; }
|
||||
|
||||
b64url() { openssl base64 -A | tr '+/' '-_' | tr -d '='; }
|
||||
|
||||
now=$(date +%s)
|
||||
header=$(printf '%s' '{"alg":"HS256","typ":"JWT"}' | b64url)
|
||||
payload=$(printf '{"iss":"%s","sub":"%s","nbf":%d,"exp":%d,"video":{"roomList":true}}' \
|
||||
"$KEY" "$KEY" "$now" "$((now + 60))" | b64url)
|
||||
sig=$(printf '%s.%s' "$header" "$payload" \
|
||||
| openssl dgst -sha256 -hmac "$SECRET" -binary | b64url)
|
||||
jwt="$header.$payload.$sig"
|
||||
|
||||
resp=$(curl -sS --max-time 10 -w '\n%{http_code}' -X POST http://127.0.0.1:7880/twirp/livekit.RoomService/ListRooms \
|
||||
-H "Authorization: Bearer $jwt" -H 'Content-Type: application/json' -d '{}') || {
|
||||
echo "LiveKit unreachable — refusing to report idle" >&2; exit 2; }
|
||||
|
||||
code=$(printf '%s' "$resp" | tail -n1)
|
||||
resp=$(printf '%s' "$resp" | sed '$d')
|
||||
|
||||
# A failed query returns no participant counts, which would read as "idle" and
|
||||
# green-light a deploy on top of a live call. Anything but a well-formed 200 is
|
||||
# treated as unknown, not as empty.
|
||||
if [ "$code" != "200" ] || ! printf '%s' "$resp" | grep -q '"rooms"'; then
|
||||
echo "LiveKit answered HTTP $code without a room list — status unknown, not deploying" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# numParticipants is omitted by protobuf JSON when zero, so absence means empty.
|
||||
total=$(printf '%s' "$resp" | grep -oE '"numParticipants":[0-9]+' | cut -d: -f2 | paste -sd+ - | bc 2>/dev/null || true)
|
||||
total=${total:-0}
|
||||
|
||||
if [ "$total" -gt 0 ]; then
|
||||
echo "EM CALL: $total participante(s) — nao deployar"
|
||||
exit 1
|
||||
fi
|
||||
echo "OCIOSO: ninguem em call"
|
||||
exit 0
|
||||
@@ -0,0 +1,120 @@
|
||||
#!/usr/bin/env bash
|
||||
# Baixa o APK compilado no GitHub, assina aqui e publica no Gitea.
|
||||
#
|
||||
# A chave de assinatura fica nesta VPS e nao sobe para o GitHub. Ela e o que
|
||||
# prova que uma atualizacao veio de nos: um APK assinado com outra chave nao
|
||||
# instala por cima do instalado, o Android recusa. Por isso o runner entrega o
|
||||
# APK sem assinatura e a assinatura acontece aqui.
|
||||
#
|
||||
# Uso: release-android.sh [run_id]
|
||||
# sem run_id, dispara um build novo e espera terminar.
|
||||
set -euo pipefail
|
||||
|
||||
REPO=${REPO:-/opt/backspace}
|
||||
GH_REPO=${GH_REPO:-syncwrld/resenhacord}
|
||||
TOOLS=/opt/ops/android-tools/build-tools
|
||||
KEYSTORE=/root/android-signing/resenhacord.keystore
|
||||
PASSFILE=/root/android-signing/password.txt
|
||||
OUT=/opt/ops/android-out
|
||||
GITEA_API=https://git.resenha.website/api/v1/repos/devsyncwrld/backspace
|
||||
|
||||
TOKEN=$(cat /root/.github-token)
|
||||
gh() { curl -s -H "Authorization: Bearer $TOKEN" -H "Accept: application/vnd.github+json" "$@"; }
|
||||
|
||||
# apksigner e um script que chama java, e esta VPS nao tem JDK instalado (nem
|
||||
# deve ter: ela roda a producao, nao compila nada). Roda num container, com as
|
||||
# ferramentas, a chave e a pasta de saida montadas. zipalign e binario nativo e
|
||||
# roda direto no host.
|
||||
apksigner() {
|
||||
docker run --rm \
|
||||
-v "$TOOLS":/tools:ro \
|
||||
-v /root/android-signing:/ks:ro \
|
||||
-v "$OUT":/out \
|
||||
eclipse-temurin:21-jdk /tools/apksigner "$@"
|
||||
}
|
||||
|
||||
for f in "$TOOLS/apksigner" "$TOOLS/zipalign" "$KEYSTORE" "$PASSFILE"; do
|
||||
[ -e "$f" ] || { echo "faltando: $f" >&2; exit 1; }
|
||||
done
|
||||
|
||||
RUN_ID=${1:-}
|
||||
if [ -z "$RUN_ID" ]; then
|
||||
echo "==> Disparando o build no GitHub"
|
||||
gh -o /dev/null -X POST \
|
||||
"https://api.github.com/repos/$GH_REPO/actions/workflows/android.yml/dispatches" \
|
||||
-d '{"ref":"main"}'
|
||||
sleep 10
|
||||
RUN_ID=$(gh "https://api.github.com/repos/$GH_REPO/actions/workflows/android.yml/runs?per_page=1" \
|
||||
| python3 -c "import sys,json;print(json.load(sys.stdin)['workflow_runs'][0]['id'])")
|
||||
echo " run=$RUN_ID"
|
||||
|
||||
echo "==> Esperando (uns 4 min)"
|
||||
for _ in $(seq 1 90); do
|
||||
read -r status conclusion < <(gh "https://api.github.com/repos/$GH_REPO/actions/runs/$RUN_ID" \
|
||||
| python3 -c "import sys,json;r=json.load(sys.stdin);print(r['status'],r['conclusion'])")
|
||||
[ "$status" = "completed" ] && break
|
||||
sleep 20
|
||||
done
|
||||
[ "${conclusion:-}" = "success" ] || { echo "build falhou: ${conclusion:-timeout}" >&2; exit 1; }
|
||||
fi
|
||||
|
||||
echo "==> Baixando o APK sem assinatura"
|
||||
mkdir -p "$OUT"
|
||||
URL=$(gh "https://api.github.com/repos/$GH_REPO/actions/runs/$RUN_ID/artifacts" \
|
||||
| python3 -c "import sys,json;a=json.load(sys.stdin)['artifacts'];print(a[0]['archive_download_url'] if a else '')")
|
||||
[ -n "$URL" ] || { echo "o run $RUN_ID nao tem artefato" >&2; exit 1; }
|
||||
curl -sL -H "Authorization: Bearer $TOKEN" "$URL" -o "$OUT/apk.zip"
|
||||
rm -f "$OUT/resenhacord-unsigned.apk"
|
||||
unzip -o -q "$OUT/apk.zip" -d "$OUT"
|
||||
|
||||
VERSION=$(python3 -c "import json;print(json.load(open('$REPO/package.json'))['version'])")
|
||||
SIGNED="$OUT/Resenhacord-$VERSION.apk"
|
||||
|
||||
echo "==> Alinhando e assinando $VERSION"
|
||||
# --key-pass omitido de proposito: sem ele o apksigner reusa a senha do
|
||||
# keystore, que e a mesma. Passar o mesmo arquivo nas duas opcoes falha — ele le
|
||||
# os arquivos de senha em sequencia e bate no fim do arquivo na segunda leitura.
|
||||
# zipalign antes do apksigner, nao depois: o alinhamento move bytes dentro do
|
||||
# zip e invalidaria a assinatura se viesse por ultimo.
|
||||
"$TOOLS/zipalign" -p -f 4 "$OUT/resenhacord-unsigned.apk" "$OUT/aligned.apk"
|
||||
apksigner sign \
|
||||
--ks /ks/resenhacord.keystore --ks-pass file:/ks/password.txt \
|
||||
--ks-key-alias resenhacord \
|
||||
--out "/out/$(basename "$SIGNED")" /out/aligned.apk
|
||||
rm -f "$OUT/aligned.apk" "$OUT/apk.zip"
|
||||
|
||||
echo "==> Conferindo a assinatura"
|
||||
apksigner verify --print-certs "/out/$(basename "$SIGNED")" | head -5
|
||||
|
||||
if [ "${1:-}" = "--no-publish" ] || [ "${PUBLISH:-1}" = "0" ]; then
|
||||
echo "APK pronto: $SIGNED"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "==> Publicando no Gitea"
|
||||
GITEA_TOKEN=$(cat /root/.gitea-token 2>/dev/null || true)
|
||||
if [ -z "$GITEA_TOKEN" ]; then
|
||||
echo "sem /root/.gitea-token — APK pronto localmente em $SIGNED"
|
||||
exit 0
|
||||
fi
|
||||
api() { curl -s -w '\n%{http_code}' -X "$1" -H "Authorization: token $GITEA_TOKEN" "$GITEA_API$2" "${@:3}"; }
|
||||
body() { sed '$d' <<<"$1"; }
|
||||
code() { tail -n1 <<<"$1"; }
|
||||
|
||||
R=$(api GET "/releases/tags/latest")
|
||||
ID=$(body "$R" | python3 -c "import sys,json;print(json.load(sys.stdin).get('id',''))" 2>/dev/null || echo "")
|
||||
[ -n "$ID" ] || { echo "release 'latest' nao encontrada no Gitea" >&2; exit 1; }
|
||||
|
||||
# Remove o APK anterior: dois anexos com o mesmo nome fazem o Gitea servir o
|
||||
# mais antigo, e quem baixasse pegaria a versao velha sem erro nenhum.
|
||||
api GET "/releases/$ID/assets" | sed '$d' \
|
||||
| python3 -c "import sys,json;[print(a['id']) for a in json.load(sys.stdin) if a['name'].endswith('.apk')]" \
|
||||
| while read -r aid; do
|
||||
[ -n "$aid" ] && api DELETE "/releases/$ID/assets/$aid" >/dev/null && echo " removido anexo $aid"
|
||||
done
|
||||
|
||||
NAME=$(basename "$SIGNED")
|
||||
R=$(api POST "/releases/$ID/assets?name=$NAME" -F "attachment=@$SIGNED")
|
||||
[ "$(code "$R")" = "201" ] || { echo "falha no upload — HTTP $(code "$R"): $(body "$R")" >&2; exit 1; }
|
||||
echo "publicado: $NAME"
|
||||
echo "https://git.resenha.website/devsyncwrld/backspace/releases/download/latest/$NAME"
|
||||
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env bash
|
||||
# Espelha o fork no GitHub e dispara a compilacao dos instaladores.
|
||||
#
|
||||
# O GitHub e usado apenas como maquina de build: ele fornece runners Windows
|
||||
# prontos, e o modulo nativo do audio (WASAPI) so compila la. O Gitea continua
|
||||
# sendo o repositorio principal.
|
||||
set -euo pipefail
|
||||
|
||||
REPO=${REPO:-/opt/backspace}
|
||||
TAG=${1:-}
|
||||
cd "$REPO"
|
||||
|
||||
if [ -z "$TAG" ]; then
|
||||
echo "uso: $0 vX.Y.Z" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# `ssh -T git@github.com` always exits 1 (GitHub refuses shell access), and
|
||||
# under `pipefail` that status wins over grep's — so piping straight into the
|
||||
# `if` reported failure even on a working key. Capture, then test.
|
||||
SSH_PROBE="$(ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new -T git@github.com 2>&1 || true)"
|
||||
if ! printf '%s' "$SSH_PROBE" | grep -q "successfully authenticated"; then
|
||||
echo "GitHub ainda nao aceita a chave desta VPS." >&2
|
||||
echo "Adicione esta chave em https://github.com/settings/keys :" >&2
|
||||
cat ~/.ssh/id_ed25519.pub >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "==> Espelhando main no GitHub"
|
||||
git push github main
|
||||
|
||||
echo "==> Marcando $TAG (e a tag que dispara o workflow release.yml)"
|
||||
git tag -f "$TAG"
|
||||
git push -f github "$TAG"
|
||||
|
||||
echo
|
||||
echo "Compilacao disparada. Acompanhe em:"
|
||||
echo " https://github.com/syncwrld/resenhacord/actions"
|
||||
echo "Os instaladores aparecem em Releases quando terminar (~15 min)."
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "backspace",
|
||||
"version": "1.0.0",
|
||||
"version": "2.1.0",
|
||||
"private": true,
|
||||
"description": "Open, self-hosted communication platform — text, voice, video, and federation",
|
||||
"description": "Open, self-hosted communication platform \u2014 text, voice, video, and federation",
|
||||
"license": "AGPL-3.0-only",
|
||||
"author": "Jannis Braun",
|
||||
"homepage": "https://github.com/TheZwiss/backspace",
|
||||
@@ -32,8 +32,9 @@
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"better-sqlite3",
|
||||
"esbuild",
|
||||
"electron",
|
||||
"electron-native-screenshare",
|
||||
"esbuild",
|
||||
"sharp"
|
||||
],
|
||||
"patchedDependencies": {
|
||||
|
||||
|
Before Width: | Height: | Size: 364 KiB After Width: | Height: | Size: 364 KiB |
|
Before Width: | Height: | Size: 368 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 955 B After Width: | Height: | Size: 712 B |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 368 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 8.8 KiB After Width: | Height: | Size: 4.4 KiB |
@@ -1,5 +1,5 @@
|
||||
appId: com.backspace.desktop
|
||||
productName: Backspace
|
||||
productName: Resenhacord
|
||||
artifactName: "${productName}-${version}-${arch}.${ext}"
|
||||
directories:
|
||||
output: dist-electron
|
||||
@@ -14,16 +14,29 @@ files:
|
||||
- "!**/node_modules/uiohook-napi/build/**"
|
||||
- "!**/node_modules/uiohook-napi/build.bak/**"
|
||||
- "!**/node_modules/uiohook-napi/bin/**"
|
||||
# electron-native-screenshare has no prebuilds — build/Release/*.node is the
|
||||
# only copy, so it must NOT be excluded the way uiohook-napi's is. asarUnpack
|
||||
# below takes it out of the archive so the loader can find it.
|
||||
asarUnpack:
|
||||
- "**/*.node"
|
||||
npmRebuild: false
|
||||
afterPack: ./scripts/afterPack.js
|
||||
publish:
|
||||
- provider: github
|
||||
owner: TheZwiss
|
||||
repo: backspace
|
||||
# Updates are served from this fork's own Gitea, not from GitHub.
|
||||
#
|
||||
# GitHub is only the build machine — it has the Windows runners the native
|
||||
# audio module needs. Its repository is private, and electron-updater against
|
||||
# a private GitHub repo would need a token shipped inside the app, which is a
|
||||
# leaked token. Gitea serves release assets to anyone, so no credential ends
|
||||
# up in the installer.
|
||||
#
|
||||
# The tag is fixed at `latest` on purpose: electron-updater fetches
|
||||
# latest.yml before it knows which version exists, so the URL cannot contain
|
||||
# a version. CI replaces that release's assets on every publish.
|
||||
- provider: generic
|
||||
url: https://git.resenha.website/devsyncwrld/backspace/releases/download/latest/
|
||||
protocols:
|
||||
- name: Backspace
|
||||
- name: Resenhacord
|
||||
schemes:
|
||||
- backspace
|
||||
mac:
|
||||
@@ -45,5 +58,15 @@ linux:
|
||||
- deb
|
||||
category: Network
|
||||
nsis:
|
||||
oneClick: false
|
||||
allowToChangeInstallationDirectory: true
|
||||
# One-click, per-user install (%LOCALAPPDATA%), the way Discord ships.
|
||||
#
|
||||
# This is what makes an update silent: a per-user install needs no elevation,
|
||||
# so electron-updater can run the installer with /S and no UAC prompt breaks
|
||||
# the flow. The assisted installer we had before (oneClick: false) showed its
|
||||
# wizard on every update and offered an all-users install, which would then
|
||||
# demand elevation on each subsequent update.
|
||||
#
|
||||
# The trade is that a fresh install no longer asks where to put the app.
|
||||
oneClick: true
|
||||
perMachine: false
|
||||
allowToChangeInstallationDirectory: false
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "@backspace/desktop",
|
||||
"version": "1.0.0",
|
||||
"version": "2.1.0",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0-only",
|
||||
"description": "Backspace",
|
||||
"description": "Resenhacord",
|
||||
"author": {
|
||||
"name": "Jannis Braun",
|
||||
"email": "backspace@backspace.chat"
|
||||
@@ -18,9 +18,10 @@
|
||||
"clean": "rm -rf dist dist-electron",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"postinstall": "electron-rebuild -f -w uiohook-napi || node -e \"console.warn('[desktop] uiohook-napi native rebuild skipped - needs build tools (make, g++, python3). Only required to RUN the desktop app; the server, web client, and Docker image are unaffected.')\""
|
||||
"postinstall": "electron-rebuild -f -w uiohook-napi,electron-native-screenshare || node -e \"console.warn('[desktop] uiohook-napi native rebuild skipped - needs build tools (make, g++, python3). Only required to RUN the desktop app; the server, web client, and Docker image are unaffected.')\""
|
||||
},
|
||||
"dependencies": {
|
||||
"electron-native-screenshare": "^1.2.0",
|
||||
"electron-updater": "^6.3.0",
|
||||
"uiohook-napi": "^1.5.5"
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Backspace — Connect</title>
|
||||
<title>Resenhacord — Conectar</title>
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&display=swap');
|
||||
|
||||
@@ -270,7 +270,7 @@
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<div class="header">
|
||||
<h1>Welcome to Backspace</h1>
|
||||
<h1>Bem-vindo ao Resenhacord</h1>
|
||||
<p>Connect to your instance</p>
|
||||
</div>
|
||||
|
||||
@@ -360,7 +360,7 @@
|
||||
// Update header copy to reflect the "switch" intent rather than the
|
||||
// first-run "welcome" framing.
|
||||
document.querySelector('.header h1').textContent = 'Switch instance';
|
||||
document.querySelector('.header p').textContent = 'Connect to a different Backspace instance, or cancel to stay.';
|
||||
document.querySelector('.header p').textContent = 'Conecte-se a outra instância, ou cancele para ficar nesta.';
|
||||
}
|
||||
}
|
||||
init();
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Backspace — Recovery</title>
|
||||
<title>Resenhacord — Recuperação</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
@@ -128,7 +128,7 @@
|
||||
<div class="content">
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<h1>Backspace couldn't load.</h1>
|
||||
<h1>O Resenhacord não conseguiu abrir.</h1>
|
||||
<p class="hint" id="hint">Loading…</p>
|
||||
|
||||
<div class="actions">
|
||||
@@ -147,7 +147,7 @@
|
||||
<pre id="diagnostic">loading…</pre>
|
||||
</details>
|
||||
|
||||
<button class="action subtle" id="btn-quit">Quit Backspace</button>
|
||||
<button class="action subtle" id="btn-quit">Fechar o Resenhacord</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -270,7 +270,7 @@
|
||||
|
||||
async function init() {
|
||||
if (!window.backspace) {
|
||||
$('hint').textContent = 'Recovery bridge unavailable. Please quit and reinstall Backspace.';
|
||||
$('hint').textContent = 'A ponte de recuperação não respondeu. Feche e reinstale o Resenhacord.';
|
||||
return;
|
||||
}
|
||||
try {
|
||||
|
||||
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 983 B After Width: | Height: | Size: 746 B |
|
Before Width: | Height: | Size: 703 B After Width: | Height: | Size: 474 B |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 836 B |
@@ -36,28 +36,36 @@ import {
|
||||
buildTrayMenuTemplate,
|
||||
buildAppMenuTemplate,
|
||||
type RecoveryState,
|
||||
RELEASES_URL,
|
||||
} from './recovery';
|
||||
import { migrateUserData } from './userDataMigration';
|
||||
|
||||
// Override Electron's package.json-derived app name so userData lives at
|
||||
// "<appData>/Backspace" instead of leaking the monorepo's "@backspace/desktop"
|
||||
// "<appData>/Resenhacord" instead of leaking the monorepo's "@backspace/desktop"
|
||||
// package name. Must run before any app.getPath('userData') consumer.
|
||||
app.setName('Backspace');
|
||||
app.setName('Resenhacord');
|
||||
|
||||
// One-time migration from the historical scoped path. After the move the old
|
||||
// folder is gone, so subsequent launches hit the old-missing no-op branch.
|
||||
// One-time migrations, oldest first, so an install that skipped a release
|
||||
// still walks the whole chain: "@backspace/desktop" → "Backspace" →
|
||||
// "Resenhacord". Each step is a no-op once its source folder is gone, and a
|
||||
// step whose target already has data leaves it alone (`new-populated`), so a
|
||||
// user who already ran the renamed build is never overwritten by stale files.
|
||||
// Without the second step the rename would have logged everyone out: the
|
||||
// instance URL and session live under userData.
|
||||
{
|
||||
const appDataDir = app.getPath('appData');
|
||||
const oldParent = path.join(appDataDir, '@backspace');
|
||||
const result = migrateUserData({
|
||||
oldDir: path.join(oldParent, 'desktop'),
|
||||
newDir: path.join(appDataDir, 'Backspace'),
|
||||
oldParent,
|
||||
});
|
||||
if (result.kind === 'migrated') {
|
||||
console.log(`[userData] migrated ${result.from} → ${result.to}`);
|
||||
} else if (result.kind === 'failed') {
|
||||
console.error('[userData] migration failed:', result.error);
|
||||
const legacyParent = path.join(appDataDir, '@backspace');
|
||||
const steps: { oldDir: string; newDir: string; oldParent: string }[] = [
|
||||
{ oldDir: path.join(legacyParent, 'desktop'), newDir: path.join(appDataDir, 'Backspace'), oldParent: legacyParent },
|
||||
{ oldDir: path.join(appDataDir, 'Backspace'), newDir: path.join(appDataDir, 'Resenhacord'), oldParent: appDataDir },
|
||||
];
|
||||
for (const step of steps) {
|
||||
const result = migrateUserData(step);
|
||||
if (result.kind === 'migrated') {
|
||||
console.log(`[userData] migrated ${result.from} → ${result.to}`);
|
||||
} else if (result.kind === 'failed') {
|
||||
console.error('[userData] migration failed:', result.error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,7 +249,7 @@ function applyLoginItemSettings(openAtLogin: boolean, startMinimized: boolean):
|
||||
enabled: openAtLogin,
|
||||
path: process.execPath,
|
||||
args: startMinimized ? ['--hidden'] : [],
|
||||
name: 'Backspace',
|
||||
name: 'Resenhacord',
|
||||
});
|
||||
} else {
|
||||
// Linux: setLoginItemSettings creates ~/.config/autostart/<name>.desktop.
|
||||
@@ -265,6 +273,102 @@ function applyLoginItemSettings(openAtLogin: boolean, startMinimized: boolean):
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Native system-audio capture ────────────────────────────────────────────
|
||||
//
|
||||
// Electron's own `audio: 'loopback'` captures the whole output mix, which
|
||||
// includes this app playing everyone else's voices — so those voices went back
|
||||
// out inside the screen share and every listener heard themselves. Not acoustic
|
||||
// echo: it is a digital copy of the output, so headphones never helped.
|
||||
//
|
||||
// This module captures with process-level isolation instead: only the shared
|
||||
// window (include mode), or everything except this app (exclude mode).
|
||||
interface NativeAudioMeta {
|
||||
sampleRate: number;
|
||||
channels: number;
|
||||
bitsPerSample: number;
|
||||
isFloat: boolean;
|
||||
}
|
||||
|
||||
interface NativeScreenShareAudio {
|
||||
startCapture(processId?: number, isIncludeMode?: boolean, onData?: (data: Buffer, meta: NativeAudioMeta) => void): boolean;
|
||||
stopCapture(): boolean;
|
||||
getPidFromWindowHandle(windowHandle: number): number;
|
||||
isAvailable(): boolean;
|
||||
getLoadError(): string | null;
|
||||
}
|
||||
|
||||
let nativeAudio: NativeScreenShareAudio | null = null;
|
||||
try {
|
||||
// Required lazily and defensively: a native module that fails to load must
|
||||
// degrade to sharing without audio, never stop the app from starting.
|
||||
nativeAudio = require('electron-native-screenshare') as NativeScreenShareAudio;
|
||||
if (!nativeAudio.isAvailable()) {
|
||||
console.warn('[Main:ScreenShare] Native audio unavailable:', nativeAudio.getLoadError());
|
||||
nativeAudio = null;
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[Main:ScreenShare] Native audio module missing:', err);
|
||||
nativeAudio = null;
|
||||
}
|
||||
|
||||
let nativeAudioActive = false;
|
||||
|
||||
/**
|
||||
* Windows and Linux hand desktopCapturer ids of the form `window:<handle>:<n>`.
|
||||
* Recovering the handle lets us capture only that window's audio, which is
|
||||
* better than excluding ourselves: a game's sound goes out, the rest of the
|
||||
* desktop does not.
|
||||
*/
|
||||
function windowHandleFromSourceId(sourceId: string): number | null {
|
||||
const match = /^window:(\d+)/.exec(sourceId);
|
||||
if (!match) return null;
|
||||
const handle = Number(match[1]);
|
||||
return Number.isFinite(handle) && handle > 0 ? handle : null;
|
||||
}
|
||||
|
||||
function startNativeAudioCapture(sourceId: string): boolean {
|
||||
if (!nativeAudio || nativeAudioActive) return false;
|
||||
|
||||
let targetPid = process.pid;
|
||||
let includeMode = false;
|
||||
|
||||
const handle = windowHandleFromSourceId(sourceId);
|
||||
if (handle !== null) {
|
||||
const pid = nativeAudio.getPidFromWindowHandle(handle);
|
||||
// pid 0 means the handle did not resolve; fall back to excluding ourselves
|
||||
// rather than capturing nothing.
|
||||
if (pid > 0) {
|
||||
targetPid = pid;
|
||||
includeMode = true;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const started = nativeAudio.startCapture(targetPid, includeMode, (data, meta) => {
|
||||
if (!mainWindow || mainWindow.isDestroyed()) return;
|
||||
mainWindow.webContents.send('native-audio-data', data, meta);
|
||||
});
|
||||
nativeAudioActive = started;
|
||||
console.log('[Main:ScreenShare] Native audio', started ? 'started' : 'failed',
|
||||
includeMode ? `(only pid ${targetPid})` : '(excluding self)');
|
||||
return started;
|
||||
} catch (err) {
|
||||
console.error('[Main:ScreenShare] Native audio start failed:', err);
|
||||
nativeAudioActive = false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function stopNativeAudioCapture(): void {
|
||||
if (!nativeAudio || !nativeAudioActive) return;
|
||||
try {
|
||||
nativeAudio.stopCapture();
|
||||
} catch (err) {
|
||||
console.warn('[Main:ScreenShare] Native audio stop failed:', err);
|
||||
}
|
||||
nativeAudioActive = false;
|
||||
}
|
||||
|
||||
// ─── Tray Icon ──────────────────────────────────────────────────────────────
|
||||
|
||||
function generateFallbackTrayIcon(): Electron.NativeImage {
|
||||
@@ -341,7 +445,7 @@ function createWindow(): void {
|
||||
: {}),
|
||||
minWidth: 940,
|
||||
minHeight: 500,
|
||||
title: 'Backspace',
|
||||
title: 'Resenhacord',
|
||||
icon: path.join(__dirname, '..', 'build', 'icon.png'),
|
||||
titleBarStyle: process.platform === 'darwin' ? 'hiddenInset' : 'hidden',
|
||||
...(process.platform !== 'darwin' ? {
|
||||
@@ -474,7 +578,7 @@ function createTray(): void {
|
||||
const icon = loadTrayIcon();
|
||||
tray = new Tray(icon);
|
||||
|
||||
tray.setToolTip('Backspace');
|
||||
tray.setToolTip('Resenhacord');
|
||||
// Context menu is set by the recoveryStore subscriber in app.whenReady,
|
||||
// which keeps the menu in sync with update/recovery state.
|
||||
|
||||
@@ -492,7 +596,9 @@ function createTray(): void {
|
||||
|
||||
function showNotification(title: string, body: string, onClick?: () => void): void {
|
||||
if (!Notification.isSupported()) return;
|
||||
const notification = new Notification({ title, body, silent: false });
|
||||
// silent: o som do sistema é o que mais incomoda numa notificação de chat, e
|
||||
// o app toca o seu próprio efeito — que combina com os demais sons dele.
|
||||
const notification = new Notification({ title, body, silent: true });
|
||||
notification.on('click', onClick ?? (() => {
|
||||
mainWindow?.show();
|
||||
mainWindow?.focus();
|
||||
@@ -576,12 +682,25 @@ function registerIpcHandlers(): void {
|
||||
ipcMain.on('install-update', () => {
|
||||
try {
|
||||
const { autoUpdater } = require('electron-updater');
|
||||
autoUpdater.quitAndInstall();
|
||||
quitAndInstallSilently(autoUpdater);
|
||||
} catch {
|
||||
// Auto-updater not available
|
||||
}
|
||||
});
|
||||
|
||||
// The `update-downloaded` push is a one-shot: it reaches whatever page is
|
||||
// loaded at that instant, and nothing else. It is lost when the window is in
|
||||
// the tray (destroyed), when the check lands before the web app has mounted
|
||||
// its listener, and on every reload. Since `downloaded` also latches off
|
||||
// further polls, the banner could then never appear at all — which is why
|
||||
// pressing "check for updates" by hand looked like the only thing that
|
||||
// worked. The renderer now asks for the current state on mount instead of
|
||||
// relying solely on catching the event live.
|
||||
ipcMain.handle('get-update-status', () => {
|
||||
const s = recoveryStore.get();
|
||||
return { state: s.updateState, version: s.updateVersion };
|
||||
});
|
||||
|
||||
ipcMain.on('check-for-updates', () => {
|
||||
try {
|
||||
const { autoUpdater } = require('electron-updater');
|
||||
@@ -594,6 +713,10 @@ function registerIpcHandlers(): void {
|
||||
ipcMain.handle('get-app-version', () => app.getVersion());
|
||||
|
||||
// Screen share picker coordination (used by setDisplayMediaRequestHandler)
|
||||
ipcMain.on('native-audio-stop', () => {
|
||||
stopNativeAudioCapture();
|
||||
});
|
||||
|
||||
ipcMain.on('screen-share-selected', (_event, _sourceId: string | null, _shareAudio?: boolean) => {
|
||||
// Handled via ipcMain.once in the display media handler — this is just
|
||||
// a safety net to prevent unhandled-message warnings
|
||||
@@ -608,7 +731,7 @@ function registerIpcHandlers(): void {
|
||||
// executableWillLaunchAtLogin to honour Task Manager's StartupApproved state.
|
||||
const osState = app.getLoginItemSettings({ path: process.execPath });
|
||||
const ownEntry = osState.launchItems?.find(
|
||||
(item) => item.name === 'Backspace' || item.path?.toLowerCase() === process.execPath.toLowerCase(),
|
||||
(item) => item.name === 'Resenhacord' || item.name === 'Backspace' || item.path?.toLowerCase() === process.execPath.toLowerCase(),
|
||||
);
|
||||
// When the Run entry exists, its args are the source of truth for startMinimized.
|
||||
// When the entry is absent (autostart is off), there is no OS state to read, so we
|
||||
@@ -643,7 +766,7 @@ function registerIpcHandlers(): void {
|
||||
if (process.platform === 'win32') {
|
||||
const osState = app.getLoginItemSettings({ path: process.execPath });
|
||||
const ownEntry = osState.launchItems?.find(
|
||||
(item) => item.name === 'Backspace' || item.path?.toLowerCase() === process.execPath.toLowerCase(),
|
||||
(item) => item.name === 'Resenhacord' || item.name === 'Backspace' || item.path?.toLowerCase() === process.execPath.toLowerCase(),
|
||||
);
|
||||
currentOpenAtLogin = osState.executableWillLaunchAtLogin ?? false;
|
||||
// See `get-auto-launch-settings`: when the entry is absent, fall back to disk
|
||||
@@ -702,6 +825,22 @@ function registerIpcHandlers(): void {
|
||||
|
||||
// ─── Auto-Update ────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Installs a staged update with no installer UI and relaunches the app.
|
||||
*
|
||||
* `quitAndInstall()` defaults to (isSilent=false, isForceRunAfter=false): on
|
||||
* Windows that runs the NSIS installer with its full wizard, so "restart to
|
||||
* update" meant clicking through a setup program. Silent mode passes /S and
|
||||
* reuses the recorded install location; isForceRunAfter brings the app back up
|
||||
* afterwards, which the wizard would otherwise leave to a checkbox.
|
||||
*
|
||||
* Both flags are inert on macOS and Linux, where the update is applied by
|
||||
* swapping files — no installer runs either way.
|
||||
*/
|
||||
function quitAndInstallSilently(au: { quitAndInstall: (isSilent?: boolean, isForceRunAfter?: boolean) => void }): void {
|
||||
au.quitAndInstall(true, true);
|
||||
}
|
||||
|
||||
function initAutoUpdater(): void {
|
||||
try {
|
||||
const { autoUpdater } = require('electron-updater');
|
||||
@@ -712,7 +851,14 @@ function initAutoUpdater(): void {
|
||||
|
||||
let updateConfirmed = false;
|
||||
|
||||
// Cadence state. `lastCheckAt` is stamped from the event rather than from
|
||||
// the callers, so a manual "check for updates" from the settings panel
|
||||
// counts too and a focus arriving right after it doesn't check twice.
|
||||
let lastCheckAt = 0;
|
||||
let downloaded = false;
|
||||
|
||||
autoUpdater.on('checking-for-update', () => {
|
||||
lastCheckAt = Date.now();
|
||||
recoveryStore.update({ updateState: 'checking', lastCheckResult: null });
|
||||
updateConfirmed = false;
|
||||
});
|
||||
@@ -734,6 +880,7 @@ function initAutoUpdater(): void {
|
||||
});
|
||||
|
||||
autoUpdater.on('update-downloaded', (info: { version: string }) => {
|
||||
downloaded = true;
|
||||
const version = info.version.slice(0, 32);
|
||||
recoveryStore.update({ updateState: 'downloaded', updateVersion: version });
|
||||
mainWindow?.webContents.send('update-downloaded', { version });
|
||||
@@ -743,9 +890,9 @@ function initAutoUpdater(): void {
|
||||
// (recovery mode) is visible — no need to also fire a native toast.
|
||||
if (!mainWindow?.isFocused()) {
|
||||
showNotification(
|
||||
'Backspace update ready',
|
||||
'Resenhacord update ready',
|
||||
`Click to restart and install version ${version}.`,
|
||||
() => autoUpdater.quitAndInstall(),
|
||||
() => quitAndInstallSilently(autoUpdater),
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -768,22 +915,33 @@ function initAutoUpdater(): void {
|
||||
if (updateConfirmed) {
|
||||
mainWindow?.webContents.send('update-error', {
|
||||
message,
|
||||
releaseUrl: 'https://github.com/TheZwiss/backspace/releases/latest',
|
||||
releaseUrl: RELEASES_URL,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Initial check with 10s delay (existing behavior)
|
||||
setTimeout(() => {
|
||||
updateConfirmed = false;
|
||||
autoUpdater.checkForUpdates().catch(() => {});
|
||||
}, 10_000);
|
||||
// A release published while the app sits open used to go unnoticed until
|
||||
// the next poll — the startup check had long since run, and four hours is
|
||||
// a long time to look at a stale version. Poll more often, and also check
|
||||
// when the user comes back to the window, which is exactly when they would
|
||||
// notice an update anyway.
|
||||
const POLL_INTERVAL_MS = 30 * 60 * 1000;
|
||||
const MIN_GAP_MS = 5 * 60 * 1000; // keeps alt-tabbing off the feed
|
||||
|
||||
// Periodic check every 4 hours (existing behavior)
|
||||
setInterval(() => {
|
||||
const maybeCheck = (): void => {
|
||||
if (downloaded) return; // already staged; nothing left to look for
|
||||
if (Date.now() - lastCheckAt < MIN_GAP_MS) return;
|
||||
updateConfirmed = false;
|
||||
autoUpdater.checkForUpdates().catch(() => {});
|
||||
}, 4 * 60 * 60 * 1000);
|
||||
};
|
||||
|
||||
setTimeout(maybeCheck, 10_000);
|
||||
setInterval(maybeCheck, POLL_INTERVAL_MS);
|
||||
|
||||
// App-level rather than mainWindow-level: the window is destroyed and
|
||||
// recreated when closed to tray and reopened, and a listener bound to the
|
||||
// old instance would go quiet after the first close.
|
||||
app.on('browser-window-focus', maybeCheck);
|
||||
} catch {
|
||||
// Auto-updater unavailable — recovery surface still functions, just
|
||||
// without update affordances. autoUpdaterRef stays null; recovery action
|
||||
@@ -953,7 +1111,19 @@ if (!gotTheLock) {
|
||||
// `PulseaudioLoopbackForScreenShare` feature flag we enable above.
|
||||
// Fails on PipeWire-only systems without pulse compat — the
|
||||
// renderer catches that and toasts the user.
|
||||
callback({ video: selected, ...(shareAudio ? { audio: 'loopback' } : {}) });
|
||||
// Audio no longer rides on the Electron stream: `loopback` would put
|
||||
// this app's own output (everyone else's voices) back into the share.
|
||||
// The native module captures it separately, isolated by process, and
|
||||
// the renderer turns it into the track LiveKit publishes.
|
||||
if (shareAudio) {
|
||||
const started = startNativeAudioCapture(sourceId);
|
||||
if (!started) {
|
||||
// Tell the renderer so it can say the share is going out silently,
|
||||
// instead of the user assuming sound is included.
|
||||
mainWindow?.webContents.send('native-audio-unavailable');
|
||||
}
|
||||
}
|
||||
callback({ video: selected });
|
||||
} catch (err) {
|
||||
console.error('[Main:ScreenShare] Handler error:', err);
|
||||
// @ts-ignore — deny the request without crashing
|
||||
@@ -971,7 +1141,7 @@ if (!gotTheLock) {
|
||||
|
||||
// AGPL-3.0 § 13: native About panel advertises the version + source repo.
|
||||
app.setAboutPanelOptions({
|
||||
applicationName: 'Backspace',
|
||||
applicationName: 'Resenhacord',
|
||||
applicationVersion: app.getVersion(),
|
||||
copyright: `AGPL-3.0-only · Source: ${UPSTREAM_SOURCE_URL}`,
|
||||
website: UPSTREAM_SOURCE_URL,
|
||||
|
||||
@@ -39,6 +39,7 @@ contextBridge.exposeInMainWorld('backspace', {
|
||||
checkForUpdates: () => {
|
||||
ipcRenderer.send('check-for-updates');
|
||||
},
|
||||
getUpdateStatus: () => ipcRenderer.invoke('get-update-status'),
|
||||
getVersion: () => ipcRenderer.invoke('get-app-version'),
|
||||
|
||||
// Window focus
|
||||
@@ -65,6 +66,24 @@ contextBridge.exposeInMainWorld('backspace', {
|
||||
onScreenShareSources: (callback: (sources: unknown[]) => void) => {
|
||||
ipcRenderer.on('screen-share-sources', (_event, sources) => callback(sources));
|
||||
},
|
||||
// Raw PCM from the native capture. Arrives ~50x/second; the renderer turns it
|
||||
// into a MediaStreamTrack for LiveKit.
|
||||
onNativeAudioData: (callback: (data: ArrayBuffer, meta: { sampleRate: number; channels: number; bitsPerSample: number; isFloat: boolean }) => void) => {
|
||||
const listener = (_event: unknown, data: Uint8Array, meta: { sampleRate: number; channels: number; bitsPerSample: number; isFloat: boolean }) => {
|
||||
// Copied out of the transferred buffer: reusing it across IPC messages
|
||||
// would let a later chunk overwrite one still being read.
|
||||
callback(data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength) as ArrayBuffer, meta);
|
||||
};
|
||||
ipcRenderer.on('native-audio-data', listener);
|
||||
return () => ipcRenderer.off('native-audio-data', listener);
|
||||
},
|
||||
onNativeAudioUnavailable: (callback: () => void) => {
|
||||
const listener = () => callback();
|
||||
ipcRenderer.on('native-audio-unavailable', listener);
|
||||
return () => ipcRenderer.off('native-audio-unavailable', listener);
|
||||
},
|
||||
stopNativeAudio: () => ipcRenderer.send('native-audio-stop'),
|
||||
|
||||
selectScreenSource: (sourceId: string | null, shareAudio?: boolean) => {
|
||||
ipcRenderer.send('screen-share-selected', sourceId, shareAudio ?? true);
|
||||
},
|
||||
|
||||
@@ -166,7 +166,7 @@ describe('buildTrayMenuTemplate', () => {
|
||||
it('includes Show/Hide/Change Instance/Quit base items', () => {
|
||||
const items = buildTrayMenuTemplate(defaultState());
|
||||
const labels = items.map((i) => i.label);
|
||||
expect(labels).toContain('Show Backspace');
|
||||
expect(labels).toContain('Mostrar o Resenhacord');
|
||||
expect(labels).toContain('Hide');
|
||||
expect(labels).toContain('Change Instance');
|
||||
expect(labels).toContain('Quit');
|
||||
|
||||
@@ -17,6 +17,15 @@ export type UpdateState =
|
||||
| 'downloaded'
|
||||
| 'error';
|
||||
|
||||
/**
|
||||
* Where a manual download lands when auto-update fails.
|
||||
*
|
||||
* The upstream GitHub repository this used to point at is private for this
|
||||
* fork, so the link 404'd for everyone who followed it. Releases are on the
|
||||
* fork's own Gitea, which is also where the updater feed lives.
|
||||
*/
|
||||
export const RELEASES_URL = 'https://git.resenha.website/devsyncwrld/backspace/releases';
|
||||
|
||||
export interface RecoveryState {
|
||||
mode: 'normal' | 'recovery';
|
||||
reason: { code: RecoveryReasonCode; detail: string } | null;
|
||||
@@ -122,7 +131,7 @@ export function buildTrayMenuTemplate(
|
||||
actions?: Partial<MenuActions>,
|
||||
): MenuItemConstructorOptions[] {
|
||||
const items: MenuItemConstructorOptions[] = [
|
||||
{ label: 'Show Backspace', click: actions?.onShow },
|
||||
{ label: 'Mostrar o Resenhacord', click: actions?.onShow },
|
||||
{ label: 'Hide', click: actions?.onHide },
|
||||
{ type: 'separator' },
|
||||
checkForUpdatesItem(state, () => actions?.onCheckForUpdates?.()),
|
||||
@@ -419,7 +428,8 @@ export function handleRecoveryAction(action: RecoveryAction): void {
|
||||
// Direct quitAndInstall — does NOT rely on autoInstallOnAppQuit.
|
||||
// Force-kill-fix: if user reaches recovery and clicks here, install
|
||||
// happens cleanly even if the on-quit hook would otherwise be bypassed.
|
||||
autoUpdaterRef?.quitAndInstall();
|
||||
// Silent + relaunch, matching the in-app banner: no installer wizard.
|
||||
autoUpdaterRef?.quitAndInstall(true, true);
|
||||
return;
|
||||
}
|
||||
case 'change-instance': {
|
||||
@@ -440,7 +450,7 @@ export function handleRecoveryAction(action: RecoveryAction): void {
|
||||
return;
|
||||
}
|
||||
case 'open-releases': {
|
||||
shell.openExternal('https://github.com/TheZwiss/backspace/releases/latest');
|
||||
shell.openExternal(RELEASES_URL);
|
||||
return;
|
||||
}
|
||||
case 'quit': {
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
node_modules/
|
||||
|
||||
# Gerados por `cap sync` a cada build — versioná-los só criaria diffs ruidosos
|
||||
# e o risco de o APK sair com uma cópia velha.
|
||||
android/app/src/main/assets/public/
|
||||
android/app/src/main/assets/capacitor.config.json
|
||||
android/app/src/main/assets/capacitor.plugins.json
|
||||
@@ -0,0 +1,101 @@
|
||||
# Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore
|
||||
|
||||
# Built application files
|
||||
*.apk
|
||||
*.aar
|
||||
*.ap_
|
||||
*.aab
|
||||
|
||||
# Files for the ART/Dalvik VM
|
||||
*.dex
|
||||
|
||||
# Java class files
|
||||
*.class
|
||||
|
||||
# Generated files
|
||||
bin/
|
||||
gen/
|
||||
out/
|
||||
# Uncomment the following line in case you need and you don't have the release build type files in your app
|
||||
# release/
|
||||
|
||||
# Gradle files
|
||||
.gradle/
|
||||
build/
|
||||
|
||||
# Local configuration file (sdk path, etc)
|
||||
local.properties
|
||||
|
||||
# Proguard folder generated by Eclipse
|
||||
proguard/
|
||||
|
||||
# Log Files
|
||||
*.log
|
||||
|
||||
# Android Studio Navigation editor temp files
|
||||
.navigation/
|
||||
|
||||
# Android Studio captures folder
|
||||
captures/
|
||||
|
||||
# IntelliJ
|
||||
*.iml
|
||||
.idea/workspace.xml
|
||||
.idea/tasks.xml
|
||||
.idea/gradle.xml
|
||||
.idea/assetWizardSettings.xml
|
||||
.idea/dictionaries
|
||||
.idea/libraries
|
||||
# Android Studio 3 in .gitignore file.
|
||||
.idea/caches
|
||||
.idea/modules.xml
|
||||
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
|
||||
.idea/navEditor.xml
|
||||
|
||||
# Keystore files
|
||||
# Uncomment the following lines if you do not want to check your keystore files in.
|
||||
#*.jks
|
||||
#*.keystore
|
||||
|
||||
# External native build folder generated in Android Studio 2.2 and later
|
||||
.externalNativeBuild
|
||||
.cxx/
|
||||
|
||||
# Google Services (e.g. APIs or Firebase)
|
||||
# google-services.json
|
||||
|
||||
# Freeline
|
||||
freeline.py
|
||||
freeline/
|
||||
freeline_project_description.json
|
||||
|
||||
# fastlane
|
||||
fastlane/report.xml
|
||||
fastlane/Preview.html
|
||||
fastlane/screenshots
|
||||
fastlane/test_output
|
||||
fastlane/readme.md
|
||||
|
||||
# Version control
|
||||
vcs.xml
|
||||
|
||||
# lint
|
||||
lint/intermediates/
|
||||
lint/generated/
|
||||
lint/outputs/
|
||||
lint/tmp/
|
||||
# lint/reports/
|
||||
|
||||
# Android Profiling
|
||||
*.hprof
|
||||
|
||||
# Cordova plugins for Capacitor
|
||||
capacitor-cordova-android-plugins
|
||||
|
||||
# Copied web assets
|
||||
app/src/main/assets/public
|
||||
|
||||
# Generated Config files
|
||||
app/src/main/assets/capacitor.config.json
|
||||
app/src/main/assets/capacitor.plugins.json
|
||||
app/src/main/res/xml/config.xml
|
||||
@@ -0,0 +1,2 @@
|
||||
/build/*
|
||||
!/build/.npmkeep
|
||||
@@ -0,0 +1,57 @@
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
namespace "com.resenhacord.app"
|
||||
compileSdk rootProject.ext.compileSdkVersion
|
||||
defaultConfig {
|
||||
applicationId "com.resenhacord.app"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
// Mantidos em sincronia com a versao do monorepo. O versionCode e o
|
||||
// numero que o Android usa para decidir se um APK e mais novo, e ele
|
||||
// precisa ser inteiro e sempre crescente: 2.1.0 -> 20100.
|
||||
versionCode 20100
|
||||
versionName "2.1.0"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
aaptOptions {
|
||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||
// Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61
|
||||
ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
flatDir{
|
||||
dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(include: ['*.jar'], dir: 'libs')
|
||||
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
|
||||
implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
|
||||
implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
|
||||
implementation project(':capacitor-android')
|
||||
testImplementation "junit:junit:$junitVersion"
|
||||
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
|
||||
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
|
||||
implementation project(':capacitor-cordova-android-plugins')
|
||||
}
|
||||
|
||||
apply from: 'capacitor.build.gradle'
|
||||
|
||||
try {
|
||||
def servicesJSON = file('google-services.json')
|
||||
if (servicesJSON.text) {
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
}
|
||||
} catch(Exception e) {
|
||||
logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work")
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
|
||||
|
||||
android {
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_21
|
||||
targetCompatibility JavaVersion.VERSION_21
|
||||
}
|
||||
}
|
||||
|
||||
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
||||
dependencies {
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
if (hasProperty('postBuildExtras')) {
|
||||
postBuildExtras()
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.getcapacitor.myapp;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
||||
*/
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class ExampleInstrumentedTest {
|
||||
|
||||
@Test
|
||||
public void useAppContext() throws Exception {
|
||||
// Context of the app under test.
|
||||
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
|
||||
|
||||
assertEquals("com.getcapacitor.app", appContext.getPackageName());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme">
|
||||
|
||||
<activity
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode|navigation"
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/title_activity_main"
|
||||
android:theme="@style/AppTheme.NoActionBarLaunch"
|
||||
android:launchMode="singleTask"
|
||||
android:exported="true">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
|
||||
<!--
|
||||
O tipo "microphone" tem de bater com o que o servico passa para o
|
||||
startForeground e com a permissao FOREGROUND_SERVICE_MICROPHONE. A
|
||||
partir do Android 14 os tres precisam concordar, senao o sistema
|
||||
derruba o app na hora de entrar na call.
|
||||
-->
|
||||
<service
|
||||
android:name=".VoiceForegroundService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="microphone" />
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths"></meta-data>
|
||||
</provider>
|
||||
</application>
|
||||
|
||||
<!-- Permissions -->
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<!--
|
||||
Voz e video. A WebView nao pede nada ao sistema sozinha: quando a pagina
|
||||
chama getUserMedia, o bridge do Capacitor intercepta o pedido e traduz
|
||||
para estas permissoes nativas. Sem declara-las aqui, a chamada falha com
|
||||
NotAllowedError e o microfone simplesmente nao liga.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<!-- Trocar entre alto-falante e fone durante a chamada. -->
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||
|
||||
<!-- Android 13+ nao mostra notificacao nenhuma sem esta permissao. -->
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<!--
|
||||
Chamada com a tela apagada. O servico em primeiro plano impede o sistema
|
||||
de congelar o processo; o wake lock impede a CPU de dormir. Sem os dois, a
|
||||
WebView para de rodar o JavaScript e a call cai.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
|
||||
<!--
|
||||
O aparelho precisa ter microfone; camera e opcional para nao excluir
|
||||
tablets sem camera frontal da lista de aparelhos compativeis.
|
||||
-->
|
||||
<uses-feature android:name="android.hardware.microphone" android:required="true" />
|
||||
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
||||
</manifest>
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.resenhacord.app;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
|
||||
import com.getcapacitor.BridgeActivity;
|
||||
|
||||
public class MainActivity extends BridgeActivity {
|
||||
|
||||
private static final int REQ_NOTIFICATIONS = 100;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
// Plugins que moram no proprio app nao sao descobertos sozinhos, e o
|
||||
// registro tem de acontecer antes do super.onCreate — depois disso a
|
||||
// ponte ja foi montada e o plugin nao aparece para o JavaScript.
|
||||
registerPlugin(VoicePlugin.class);
|
||||
super.onCreate(savedInstanceState);
|
||||
requestNotificationPermission();
|
||||
}
|
||||
|
||||
/**
|
||||
* A partir do Android 13 a notificacao da chamada nao aparece sem esta
|
||||
* permissao. O servico continua rodando de qualquer forma — a chamada nao
|
||||
* cai se o usuario recusar —, mas ele fica sem nada visivel na barra, o que
|
||||
* so parece defeito. Por isso perguntamos, e seguimos com a recusa.
|
||||
*/
|
||||
private void requestNotificationPermission() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return;
|
||||
if (checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS)
|
||||
== PackageManager.PERMISSION_GRANTED) return;
|
||||
requestPermissions(new String[]{Manifest.permission.POST_NOTIFICATIONS}, REQ_NOTIFICATIONS);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
package com.resenhacord.app;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ServiceInfo;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
import android.os.PowerManager;
|
||||
|
||||
/**
|
||||
* Mantem o app vivo enquanto ha uma chamada em andamento.
|
||||
*
|
||||
* Sem isto, trancar a tela derruba a call. O Android congela processos em
|
||||
* segundo plano — e uma WebView congelada para de rodar o JavaScript que
|
||||
* mantem a conexao do LiveKit de pe. Um servico em primeiro plano e a unica
|
||||
* coisa que o sistema aceita como "este app precisa continuar rodando", e ele
|
||||
* exige a notificacao permanente que aparece junto: e o preco combinado, o
|
||||
* usuario sempre ve que algo esta ativo.
|
||||
*
|
||||
* O wake lock parcial e separado e igualmente necessario: o servico impede o
|
||||
* congelamento do processo, mas nao impede a CPU de dormir.
|
||||
*/
|
||||
public class VoiceForegroundService extends Service {
|
||||
|
||||
public static final String ACTION_START = "com.resenhacord.app.VOICE_START";
|
||||
public static final String ACTION_STOP = "com.resenhacord.app.VOICE_STOP";
|
||||
public static final String ACTION_HANGUP = "com.resenhacord.app.VOICE_HANGUP";
|
||||
public static final String EXTRA_CHANNEL = "channel";
|
||||
|
||||
private static final String CHANNEL_ID = "voice_call";
|
||||
private static final int NOTIFICATION_ID = 1;
|
||||
|
||||
private PowerManager.WakeLock wakeLock;
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
String action = intent != null ? intent.getAction() : null;
|
||||
|
||||
if (ACTION_STOP.equals(action)) {
|
||||
stop();
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
if (ACTION_HANGUP.equals(action)) {
|
||||
// O botao da notificacao nao desliga a chamada sozinho: quem sabe
|
||||
// como sair da sala e o app web. Avisamos o JavaScript e deixamos
|
||||
// ele encerrar; o servico para quando o JS confirmar a saida.
|
||||
VoicePlugin.emitHangup();
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
String channel = intent != null ? intent.getStringExtra(EXTRA_CHANNEL) : null;
|
||||
startForegroundWithNotification(channel);
|
||||
acquireWakeLock();
|
||||
return START_STICKY;
|
||||
}
|
||||
|
||||
private void startForegroundWithNotification(String channelName) {
|
||||
createNotificationChannel();
|
||||
|
||||
Intent open = new Intent(this, MainActivity.class);
|
||||
open.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
PendingIntent openPending = PendingIntent.getActivity(
|
||||
this, 0, open, PendingIntent.FLAG_IMMUTABLE);
|
||||
|
||||
Intent hangup = new Intent(this, VoiceForegroundService.class);
|
||||
hangup.setAction(ACTION_HANGUP);
|
||||
PendingIntent hangupPending = PendingIntent.getService(
|
||||
this, 1, hangup, PendingIntent.FLAG_IMMUTABLE);
|
||||
|
||||
String text = channelName != null && !channelName.isEmpty()
|
||||
? channelName
|
||||
: getString(R.string.voice_notification_text);
|
||||
|
||||
Notification notification = new Notification.Builder(this, CHANNEL_ID)
|
||||
.setContentTitle(getString(R.string.voice_notification_title))
|
||||
.setContentText(text)
|
||||
.setSmallIcon(R.drawable.ic_stat_voice)
|
||||
.setContentIntent(openPending)
|
||||
.addAction(new Notification.Action.Builder(
|
||||
null, getString(R.string.voice_notification_hangup), hangupPending).build())
|
||||
.setOngoing(true)
|
||||
.setUsesChronometer(true)
|
||||
.build();
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
// O tipo e obrigatorio a partir do Android 14: sem ele o sistema
|
||||
// recusa o servico e derruba o app com uma excecao.
|
||||
startForeground(NOTIFICATION_ID, notification,
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE);
|
||||
} else {
|
||||
startForeground(NOTIFICATION_ID, notification);
|
||||
}
|
||||
}
|
||||
|
||||
private void createNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return;
|
||||
NotificationManager manager = getSystemService(NotificationManager.class);
|
||||
if (manager.getNotificationChannel(CHANNEL_ID) != null) return;
|
||||
|
||||
// Importancia baixa de proposito: a notificacao existe porque o sistema
|
||||
// exige, nao para chamar atencao. Som ou vibracao aqui tocariam a cada
|
||||
// entrada em call, por cima da propria chamada.
|
||||
NotificationChannel channel = new NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
getString(R.string.voice_channel_name),
|
||||
NotificationManager.IMPORTANCE_LOW);
|
||||
channel.setDescription(getString(R.string.voice_channel_description));
|
||||
channel.setShowBadge(false);
|
||||
manager.createNotificationChannel(channel);
|
||||
}
|
||||
|
||||
private void acquireWakeLock() {
|
||||
if (wakeLock != null && wakeLock.isHeld()) return;
|
||||
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
|
||||
wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Resenhacord::voice");
|
||||
// Sem timeout infinito: se o app morrer de um jeito que nao passe pelo
|
||||
// stop(), o sistema solta o lock sozinho depois de duas horas em vez de
|
||||
// segurar a CPU para sempre.
|
||||
wakeLock.acquire(2 * 60 * 60 * 1000L);
|
||||
}
|
||||
|
||||
private void releaseWakeLock() {
|
||||
if (wakeLock != null && wakeLock.isHeld()) {
|
||||
wakeLock.release();
|
||||
}
|
||||
wakeLock = null;
|
||||
}
|
||||
|
||||
private void stop() {
|
||||
releaseWakeLock();
|
||||
stopForeground(true);
|
||||
stopSelf();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
releaseWakeLock();
|
||||
super.onDestroy();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.resenhacord.app;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
|
||||
import com.getcapacitor.JSObject;
|
||||
import com.getcapacitor.Plugin;
|
||||
import com.getcapacitor.PluginCall;
|
||||
import com.getcapacitor.PluginMethod;
|
||||
import com.getcapacitor.annotation.CapacitorPlugin;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
/**
|
||||
* Ponte entre o app web e o servico de chamada.
|
||||
*
|
||||
* O app web e quem sabe quando ha uma call: ele fala com o LiveKit. O lado
|
||||
* nativo e quem sabe pedir ao Android para nao congelar o processo. Este plugin
|
||||
* so liga as duas pontas.
|
||||
*/
|
||||
@CapacitorPlugin(name = "VoiceForeground")
|
||||
public class VoicePlugin extends Plugin {
|
||||
|
||||
/**
|
||||
* Referencia fraca de proposito: o servico precisa avisar o JavaScript
|
||||
* quando alguem toca em "Sair" na notificacao, mas ele nao pode ser o
|
||||
* motivo de a instancia do plugin (e a Activity presa a ela) continuar
|
||||
* viva depois de destruida.
|
||||
*/
|
||||
private static WeakReference<VoicePlugin> instance = new WeakReference<>(null);
|
||||
|
||||
@Override
|
||||
public void load() {
|
||||
instance = new WeakReference<>(this);
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void start(PluginCall call) {
|
||||
Intent intent = new Intent(getContext(), VoiceForegroundService.class);
|
||||
intent.setAction(VoiceForegroundService.ACTION_START);
|
||||
intent.putExtra(VoiceForegroundService.EXTRA_CHANNEL, call.getString("channel"));
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
getContext().startForegroundService(intent);
|
||||
} else {
|
||||
getContext().startService(intent);
|
||||
}
|
||||
call.resolve();
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void stop(PluginCall call) {
|
||||
Intent intent = new Intent(getContext(), VoiceForegroundService.class);
|
||||
intent.setAction(VoiceForegroundService.ACTION_STOP);
|
||||
getContext().startService(intent);
|
||||
call.resolve();
|
||||
}
|
||||
|
||||
/** Chamado pelo servico quando o botao "Sair" da notificacao e tocado. */
|
||||
static void emitHangup() {
|
||||
VoicePlugin plugin = instance.get();
|
||||
if (plugin != null) {
|
||||
plugin.notifyListeners("hangup", new JSObject());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 17 KiB |
@@ -0,0 +1,34 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportHeight="108"
|
||||
android:viewportWidth="108">
|
||||
<path
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
|
||||
android:strokeColor="#00000000"
|
||||
android:strokeWidth="1">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="78.5885"
|
||||
android:endY="90.9159"
|
||||
android:startX="48.7653"
|
||||
android:startY="61.0927"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
|
||||
android:strokeColor="#00000000"
|
||||
android:strokeWidth="1" />
|
||||
</vector>
|
||||
@@ -0,0 +1,170 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportHeight="108"
|
||||
android:viewportWidth="108">
|
||||
<path
|
||||
android:fillColor="#26A69A"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,39L108,39"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,49L108,49"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,59L108,59"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,69L108,69"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,79L108,79"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,89L108,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,99L108,99"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,29L89,29"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,39L89,39"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,49L89,49"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,59L89,59"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,69L89,69"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,79L89,79"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,19L29,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,19L39,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,19L49,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,19L59,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,19L69,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,19L79,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
</vector>
|
||||
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Icone pequeno da notificacao. O Android descarta a cor e usa so o formato
|
||||
(a partir do Lollipop ele e pintado de branco sobre a silhueta), entao aqui
|
||||
vale desenho solido e simples — um microfone.
|
||||
-->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="#FFFFFF">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M12,14c1.66,0 3,-1.34 3,-3V5c0,-1.66 -1.34,-3 -3,-3S9,3.34 9,5v6c0,1.66 1.34,3 3,3z"/>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M17,11c0,2.76 -2.24,5 -5,5s-5,-2.24 -5,-5H5c0,3.53 2.61,6.43 6,6.92V21h2v-3.08c3.39,-0.49 6,-3.39 6,-6.92h-2z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Tela de abertura como camadas em vez das splash.png que o Capacitor gera para
|
||||
cada densidade e orientacao. Duas razoes: a cor de fundo fica exata em
|
||||
qualquer aparelho (as PNGs padrao sao claras e piscavam branco antes da
|
||||
WebView pintar), e a marca escala sozinha sem manter doze arquivos.
|
||||
-->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@color/brand_background" />
|
||||
<item
|
||||
android:drawable="@drawable/brand_mark"
|
||||
android:gravity="center"
|
||||
android:width="128dp"
|
||||
android:height="128dp" />
|
||||
</layer-list>
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".MainActivity">
|
||||
|
||||
<WebView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 7.1 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 7.4 KiB |
|
After Width: | Height: | Size: 16 KiB |
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- theme_color / background_color do manifest do PWA, para a abertura
|
||||
nativa e a WebView comecarem na mesma cor. -->
|
||||
<color name="brand_background">#0B0B10</color>
|
||||
<color name="colorPrimary">#0B0B10</color>
|
||||
<color name="colorPrimaryDark">#0B0B10</color>
|
||||
<color name="colorAccent">#A78BFA</color>
|
||||
</resources>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- A mesma base escura da marca; o icone adaptativo recorta o primeiro
|
||||
plano sobre ela em qualquer formato que o launcher use. -->
|
||||
<color name="ic_launcher_background">#0B0B10</color>
|
||||
</resources>
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<resources>
|
||||
<string name="app_name">Resenhacord</string>
|
||||
<string name="title_activity_main">Resenhacord</string>
|
||||
<string name="package_name">com.resenhacord.app</string>
|
||||
<string name="custom_url_scheme">com.resenhacord.app</string>
|
||||
<string name="voice_channel_name">Chamada em andamento</string>
|
||||
<string name="voice_channel_description">Mantem a chamada de voz ativa com o app em segundo plano</string>
|
||||
<string name="voice_notification_title">Resenhacord</string>
|
||||
<string name="voice_notification_text">Em call</string>
|
||||
<string name="voice_notification_hangup">Sair</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<!-- Base application theme. -->
|
||||
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
<item name="colorPrimary">@color/colorPrimary</item>
|
||||
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||
<item name="colorAccent">@color/colorAccent</item>
|
||||
</style>
|
||||
|
||||
<style name="AppTheme.NoActionBar" parent="Theme.AppCompat.DayNight.NoActionBar">
|
||||
<item name="windowActionBar">false</item>
|
||||
<item name="windowNoTitle">true</item>
|
||||
<item name="android:background">@null</item>
|
||||
</style>
|
||||
|
||||
|
||||
<style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
|
||||
<item name="android:background">@drawable/splash</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<external-path name="my_images" path="." />
|
||||
<cache-path name="my_cache_images" path="." />
|
||||
</paths>
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.getcapacitor.myapp;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
||||
*/
|
||||
public class ExampleUnitTest {
|
||||
|
||||
@Test
|
||||
public void addition_isCorrect() throws Exception {
|
||||
assertEquals(4, 2 + 2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
|
||||
buildscript {
|
||||
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:8.7.2'
|
||||
classpath 'com.google.gms:google-services:4.4.2'
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
}
|
||||
}
|
||||
|
||||
apply from: "variables.gradle"
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
|
||||
include ':capacitor-android'
|
||||
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')
|
||||
@@ -0,0 +1,22 @@
|
||||
# Project-wide Gradle settings.
|
||||
|
||||
# IDE (e.g. Android Studio) users:
|
||||
# Gradle settings configured through the IDE *will override*
|
||||
# any settings specified in this file.
|
||||
|
||||
# For more details on how to configure your build environment visit
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
org.gradle.jvmargs=-Xmx1536m
|
||||
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
# org.gradle.parallel=true
|
||||
|
||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||
# Android operating system, and which are packaged with your app's APK
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
android.useAndroidX=true
|
||||
@@ -0,0 +1,7 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
@@ -0,0 +1,252 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
|
||||
' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
@@ -0,0 +1,94 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
@@ -0,0 +1,5 @@
|
||||
include ':app'
|
||||
include ':capacitor-cordova-android-plugins'
|
||||
project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/')
|
||||
|
||||
apply from: 'capacitor.settings.gradle'
|
||||
@@ -0,0 +1,16 @@
|
||||
ext {
|
||||
minSdkVersion = 23
|
||||
compileSdkVersion = 35
|
||||
targetSdkVersion = 35
|
||||
androidxActivityVersion = '1.9.2'
|
||||
androidxAppCompatVersion = '1.7.0'
|
||||
androidxCoordinatorLayoutVersion = '1.2.0'
|
||||
androidxCoreVersion = '1.15.0'
|
||||
androidxFragmentVersion = '1.8.4'
|
||||
coreSplashScreenVersion = '1.0.1'
|
||||
androidxWebkitVersion = '1.12.1'
|
||||
junitVersion = '4.13.2'
|
||||
androidxJunitVersion = '1.2.1'
|
||||
androidxEspressoCoreVersion = '3.6.1'
|
||||
cordovaAndroidVersion = '10.1.1'
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { CapacitorConfig } from '@capacitor/cli';
|
||||
|
||||
/**
|
||||
* O APK e uma casca fina: a WebView carrega a instancia pela rede em vez de
|
||||
* servir uma copia do app web embutida no pacote.
|
||||
*
|
||||
* Duas razoes, nesta ordem:
|
||||
*
|
||||
* 1. Atualizacao. O site muda a cada deploy. Com os arquivos embutidos, cada
|
||||
* mudanca no app web exigiria republicar e reinstalar o APK a mao — nao ha
|
||||
* updater automatico no Android como o electron-updater faz no desktop.
|
||||
* Pela rede, o telefone recebe a mesma versao que o navegador, na hora.
|
||||
*
|
||||
* 2. Origem. O cliente fala com /api e /ws na mesma origem. Embutido, a pagina
|
||||
* rodaria em https://localhost e toda chamada viraria cross-origin, o que
|
||||
* obrigaria a abrir CORS no servidor para uma origem que qualquer app pode
|
||||
* reivindicar.
|
||||
*
|
||||
* O preco e nao funcionar offline, o que para um app de chat em tempo real nao
|
||||
* e perda: sem servidor nao ha o que mostrar. A pagina em public/ so aparece se
|
||||
* a WebView nao alcancar o servidor.
|
||||
*/
|
||||
const config: CapacitorConfig = {
|
||||
appId: 'com.resenhacord.app',
|
||||
appName: 'Resenhacord',
|
||||
webDir: 'public',
|
||||
android: {
|
||||
// A captura de audio e video da WebView e concedida pelo bridge do
|
||||
// Capacitor, que traduz o pedido do getUserMedia para a permissao nativa.
|
||||
allowMixedContent: false,
|
||||
},
|
||||
server: {
|
||||
url: 'https://chat.resenha.website',
|
||||
// Sem cleartext: a instancia e servida por HTTPS (Caddy), e o WebRTC do
|
||||
// LiveKit exige contexto seguro de qualquer forma.
|
||||
cleartext: false,
|
||||
androidScheme: 'https',
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "@backspace/mobile",
|
||||
"version": "2.1.0",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0-only",
|
||||
"description": "Resenhacord para Android",
|
||||
"scripts": {
|
||||
"sync": "cap sync android",
|
||||
"open": "cap open android",
|
||||
"build:debug": "cd android && ./gradlew assembleDebug",
|
||||
"build:release": "cd android && ./gradlew assembleRelease"
|
||||
},
|
||||
"dependencies": {
|
||||
"@capacitor/android": "^7.0.0",
|
||||
"@capacitor/core": "^7.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@capacitor/cli": "^7.0.0",
|
||||
"typescript": "^5.7.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<!doctype html>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>Resenhacord</title>
|
||||
<style>
|
||||
:root { color-scheme: dark; }
|
||||
body {
|
||||
margin: 0; min-height: 100dvh;
|
||||
display: grid; place-items: center; padding: 24px;
|
||||
background: #0b0b10; color: #e7e7ee;
|
||||
font: 15px/1.5 system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
|
||||
text-align: center;
|
||||
}
|
||||
h1 { font-size: 19px; margin: 0 0 8px; }
|
||||
p { margin: 0; color: #9a9aab; max-width: 30ch; }
|
||||
</style>
|
||||
<h1>Sem conexão</h1>
|
||||
<p>Não deu pra falar com o servidor. Confere a internet e abre de novo.</p>
|
||||
@@ -0,0 +1,4 @@
|
||||
ALTER TABLE `messages` ADD `pinned_at` integer;--> statement-breakpoint
|
||||
ALTER TABLE `messages` ADD `pinned_by` text;--> statement-breakpoint
|
||||
CREATE INDEX `idx_messages_channel_pinned` ON `messages` (`channel_id`,`pinned_at`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_messages_channel_user_created` ON `messages` (`channel_id`,`user_id`,`created_at`);
|
||||
@@ -0,0 +1,26 @@
|
||||
CREATE TABLE `space_emojis` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`space_id` text NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`filename` text NOT NULL,
|
||||
`uploader_id` text,
|
||||
`created_at` integer NOT NULL,
|
||||
FOREIGN KEY (`space_id`) REFERENCES `spaces`(`id`) ON UPDATE no action ON DELETE cascade,
|
||||
FOREIGN KEY (`uploader_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE set null
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `space_stickers` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`space_id` text NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`filename` text NOT NULL,
|
||||
`uploader_id` text,
|
||||
`created_at` integer NOT NULL,
|
||||
FOREIGN KEY (`space_id`) REFERENCES `spaces`(`id`) ON UPDATE no action ON DELETE cascade,
|
||||
FOREIGN KEY (`uploader_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE set null
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE `messages` ADD `sticker_id` text;--> statement-breakpoint
|
||||
CREATE INDEX `idx_space_emojis_space` ON `space_emojis` (`space_id`);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `idx_space_emojis_space_name` ON `space_emojis` (`space_id`,`name`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_space_stickers_space` ON `space_stickers` (`space_id`);
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE `messages` ADD `type` text DEFAULT 'user' NOT NULL;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE `users` ADD `profile_style` text;
|
||||