Skip to content

feat: port desktop/Electron + refactors (Pinia, TS, Vuetify 4)#49

Open
juanaleixo wants to merge 186 commits into
louvorja:mainfrom
juanaleixo:main
Open

feat: port desktop/Electron + refactors (Pinia, TS, Vuetify 4)#49
juanaleixo wants to merge 186 commits into
louvorja:mainfrom
juanaleixo:main

Conversation

@juanaleixo

Copy link
Copy Markdown
Collaborator

Summary

Porta todo o trabalho acumulado em juanaleixo/louvorja:main para o repositório principal. Inclui a versão desktop (Electron), múltiplos refactors estruturais e novas features.

Principais temas

  • Electron desktop: build matrix Windows/macOS/Linux na release, kiosk fullscreen, hash router, protocolo louvorja://, AppMenu, splash, ícones, Service Worker cleanup.
  • Sync: migração FTP → HTTPS, downloader em paralelo (6 workers), Hinário Adventista, refresh manual.
  • Liturgia: refactor por dia, UI estilo Delphi, fixes de checkbox/IDs.
  • Projeção: projeção genérica de módulos, ribbon contextual, sincronização entre janelas, modo fullscreen.
  • Música: Editor de Músicas, biblioteca de áudio, Coletâneas Pessoais.
  • Transmissão: servidor HTTP serve SPA + bridge SSE para clientes remotos.
  • Stack: migração Vuex → Pinia, helpers/composables → TypeScript, Vuetify 4 (themes aplicam às variáveis CSS).
  • Estabilidade: fixes de memory leaks, race conditions, IPv6 auth, fallbacks.
  • Tooling: Vitest + Playwright + Percy, Vite aliases, manualChunks, bundle visualizer, CI workflow.

Escopo

  • 406 arquivos alterados (+50.982 / -13.669)
  • ~150 commits desde o ancestral comum (7e93b00 — 1.27.0)

Inclui o merge #1 (Gerenciamento de abas de módulo) feito hoje.

Test plan

  • Build Electron Windows/macOS/Linux pela CI
  • Smoke test desktop: splash → shell → módulos principais
  • Projeção em fullscreen + retorno + operador
  • Liturgia: criar/editar/checar itens
  • Sync: download via HTTPS, Hinário Adventista
  • Música: editor, biblioteca de áudio
  • Transmissão: cliente remoto via SSE
  • Vitest + Playwright passando

juanaleixo and others added 30 commits May 3, 2026 10:13
…hadow

- Renames src/helpers/String.js -> src/helpers/Strings.js
- Updates import identifier in src/main.js to avoid shadowing native String class
- Public API ($string global) unchanged

Closes plans/refactor-2026/tasks/007-rename-string-helper.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- AppData.js: add toggle() as canonical method; toogle() becomes
  deprecated alias with one-time console.warn in DEV mode
- Dev.js: same fix — toggle() added, toogle() marked deprecated
- App.vue: migrate the only caller ($dev.toogle → $dev.toggle)

Zero active callers of toogle() remain. Alias preserved for
backwards-compat until a cleanup task removes it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Theme.js: dead code — $theme registered in globalProperties but
  never called anywhere. Import and registration removed from main.js.
- Window.js: single-call wrapper over window.open(). Logic inlined
  directly into Popup.js (its only consumer); file deleted.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- App.vue: remove phantom v-btn (v-show=false), handleKeydown method,
  and console.log("click ") — file reduced to minimal shell
- main.js: register Ctrl+Alt+D in the Hotkeys system (group: system),
  calling Dev.toggle() directly
- pt.json / es.json: add hotkeys.ctrl_alt_d translation key

The shortcut continues working; now appears in the F1 cheatsheet.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ModuleManager.js:
- Remove commented-out category-creation block (/* if (!moduleGroups...) */)
- Remove installRemoteModule() placeholder method — calls fetchModuleManifest()
  and downloadModuleModule() which do not exist anywhere in the codebase
- Remove empty try/catch with commented-out installRemoteModule call

main.js:
- Remove 3-line stale rationale comment above globalProperties assignment
  ("Equivalente ao mixin.beforeCreate...") — historical noise, not load-bearing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
A RFC previa regressão para v3, mas ao executar constatou-se que
Vuetify 4.0.6 é a versão `latest` no npm (non-prerelease). O critério
de reconsideração foi atingido — downgrade passou a ser custo sem
benefício. Decisão: manter v4, travar com tilde (~4.0.6).

- package.json: ^4.0.0 → ~4.0.6
- v-number-input mantido (disponível em v4, sem substituição necessária)
- ADR criado em docs/adr/0001-vuetify-versao-estavel.md
- Build e lint passam sem erros

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Audited 10 suspicious deps; removed 4 with zero usage:
- core-js: no imports; Vite targets modern browsers natively
- roboto-fontface: Roboto loaded via Google Fonts CDN (webfontloader), npm package never imported
- dotenv: Vite loads .env automatically; no require/import found
- archiver: zero references in src/ or electron/

Kept: webfontloader (src/plugins), vue-country-flag-next (LanguageSelector),
vue-json-pretty (dev module), express/basic-ftp/electron-updater (Electron
main process — must stay in dependencies for electron-builder bundling).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Separates framework vendors into dedicated chunks for better browser
caching. Main index.js drops from 380 kB → 162 kB (gzip: 135 → 55 kB).

- vendor-vue: vue + vue-router + vuex (164 kB)
- vendor-i18n: vue-i18n (54 kB)
- vendor-fuse: fuse.js (24 kB)
- Vuetify kept out of manualChunks — vite-plugin-vuetify autoImport
  does tree-shaking automatically; forcing a single chunk would break it.

Also adds rollup-plugin-visualizer (devDep) generating dist/stats.html
on every build. Fixed vite.config.js to use async + dynamic import() for
the ESM-only visualizer package, and migrated require("path") to ESM import.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds dedicated aliases to vite.config.js and jsconfig.json for the
main src/ subdirectories. Existing @/* alias is preserved for
backwards compatibility. Imports are not migrated here — that is
covered by task #097.

New aliases: @helpers, @components, @modules, @layout, @views,
@store, @lang

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Port 5002 is deliberate: electron/main.cjs hardcodes DEV_URL to
http://localhost:5002 and the electron:dev script uses wait-on with
that same URL. Changing it requires updating three files in sync.

Added port to npm run dev/host descriptions and a note explaining
why 5002 is used instead of Vite's default 5173.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Creates LiturgyTimer.vue: renders the timer display (running state,
  time, prev/next navigation) and start/stop button via props + emits.
- Index.vue replaces ~25 lines of inline timer template with
  <LiturgyTimer> + event bindings; removes 40 lines of timer CSS.
- useLiturgyTimer remains in Index.vue setup() to keep timerCurrentIndex
  and timerItemProgress available for item list highlighting (017c).
- Also includes 017a composables (useLiturgyItems, useLiturgyPersistence,
  useLiturgyTimer) which were created but not yet committed.
- Build: clean. Lint: no new errors.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Creates LiturgyItem.vue (418 lines): renders a single liturgy row —
  either a categoria header or a normal item card, controlled by props.
- Props: element, index, locked, timerActive, timerProgress, defaultColor,
  isChecked (fn), iconFor (fn), subtitleFor (fn).
- Emits: edit, remove, execute, play-music, change-color, toggle-checked.
- All lit-card* and lit-category* CSS moved to LiturgyItem.vue (scoped).
- .liturgy-list-area--locked .lit-card replaced by .lit-card--locked
  modifier handled inside the component, removing the parent-selector.
- Removes unused manifest import from Index.vue.
- Index.vue: 1661 → 1302 lines (-359 lines). Build and lint: clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Creates LiturgyItemForm.vue (254 lines of template + CSS): renders the
  add/edit item dialog with all 6 item types (anotacao, site, arquivo,
  musica, itensagendados, categoria). Uses :value + @input/@change event
  handlers instead of v-model to avoid mutating props; mutations go through
  setFormField(field, value) function prop — passes vue/no-mutating-props.
- Creates LiturgyImportExport.vue (82 lines): wraps save + load buttons and
  the hidden <input type="file">; emits @save and @file-load(event);
  removes loadFile() method and $refs.fileInput from Index.vue.
- useLiturgyItems: adds setFormField(field, value) closure function,
  exposed and passed as a prop to LiturgyItemForm.
- Index.vue: dialog CSS block (~150 lines) moved to LiturgyItemForm.vue;
  retains only .lit-input and .lit-hint (used in schedules dialog).
- Index.vue: 1302 → 960 lines (-342 lines). Build and lint: clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…up Index.vue

Index.vue: 958 → 195 lines (< 200 target). Composables receberam helpers
setActiveCatId, setNoteDayIndex, toggleNotes, toggleMenuOpen, closeMenu e
saveCategoryName agora aceita nome como parâmetro para evitar acoplamento de ref.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
helpers/Media.js removido; conteúdo movido para src/composables/useMedia.js.
Todos os consumidores (CommandRegistry, Shortcuts, main.js, useLiturgyItems)
atualizam import. $media removido de globalProperties — zero usos de this.$media.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move all reactive state (media, slides, buttons, menu_modes, etc.) and
actions (play/pause/prev/next/seek/etc.) from Player.vue into
src/composables/usePlayerState.js. Player.vue keeps the same template
and now uses setup() to spread the composable.

Methods that previously called this.\$media now call useMedia (imported
as Media) directly — fixes runtime breakage introduced by 016e (which
removed \$media from globalProperties).

Player.vue: 457 → 247 lines (script: 226 → 14). usePlayerState.js: 247.
No behavioral change. Build + lint pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extracted from Player.vue:
- PlayerControls.vue (29 lines): button row driven by `buttons` prop.
- PlayerProgress.vue (54 lines): progress bar + timestamps + slide
  counter. Receives scalar props (progress, currentTime, duration,
  buffered, etc.) and emits `seek` on click. v-model deliberately
  replaced by :model-value to avoid prop mutation; seek is the only
  interactive path.

Player.vue script switched to a setup() that exposes a `compactButtons`
computed so the compact-menu list can iterate without inline filter
syntax. Player.vue: 247 → 231 lines.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- PlayerGauge.vue (35 lines): mute toggle button + volume slider.
  Forwards `update:model-value` from v-progress-linear as `seek`
  event so the parent calls setVolume with the new value.
- PlayerActions.vue (142 lines): right-column toolbar (mode menu,
  slide jump menu, maximize/fullscreen/screen/close buttons,
  compact menu). Imports LScreenBtn locally so Player.vue does not
  need to.
- Player.vue: removed dead `location == 'footer'` blocks (Footer.vue
  has its own Delphi-style player UI now; only `window` and
  `fullscreen` callers remain). Result: 231 → 74 lines (< 100 ✅).

usePlayerState.setVolume now accepts an optional value so the gauge
can pass the click-position volume directly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- BACKLOG.md: 022 status todo → done.
- 022-refatorar-player.md: status frontmatter + detailed sub-PR notes
  documenting what was extracted, surprises encountered (016e \$media
  regression, no VU meter to extract, needed PlayerActions.vue beyond
  spec to hit < 100 lines), and runtime smoke caveat.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
mutations.js:
- _walkSet() shared utility (DRY — reused by SET_MODULE_PATH,
  SET_USER_DATA_PATH and the deprecated fallbacks).
- Named scalar mutations: SET_LOADING, SET_IS_DARK, SET_IS_DEV,
  SET_IS_POPUP, SET_IS_MOBILE, SET_IS_DESKTOP, SET_IS_ONLINE,
  SET_IMPORT_MODULES, SET_POPUP, SET_POPUP_MODULE, SET_MODULE_GROUP.
- PATCH_ALERT({ key, value }) — single-field update on state.alert.
- SET_MODULE_PATH({ id, path, value }) — generic typed mutation for
  any state.modules[id].<path> write.
- SET_USER_DATA_PATH({ path, value }) — generic typed mutation for
  any state.user_data.<path> write.
- setData / addElementArray / removeElementArray: kept as deprecated
  fallbacks with DEV console.warn showing the offending key path.

AppData.js:
- set() replaced the one direct commit("setData") with a routing
  table. Known root keys dispatch named mutations; anything else
  (dynamic paths from Popup.vue message relay, etc.) falls through to
  setData with its deprecation warning.
- No behavioral change; popup sync code unchanged.

Result: commit("setData", ...) zero first-level callers outside
AppData.js itself. DevTools now shows meaningful mutation names.
Build + lint pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Fix critical normalization bug in Hotkeys.js: _comboFromEvent now
  lowercases all keys unconditionally, matching _normalizeCombo. This
  made every multi-char shortcut (F1, Space, Escape, ArrowLeft, Home,
  Delete, etc.) previously registered in main.js non-functional.

- Remove vue3-shortkey: uninstall package, drop import/app.use from
  main.js, strip v-shortkey + @shortkey from PlayerControls.vue and
  PlayerActions.vue, remove shortkey fields from usePlayerState buttons.

- Migrate Bible shortcuts (←, →, Del) to Hotkeys.register in
  mounted()/unmounted() of Bible/Index.vue. Handler refs stored on
  `this` for targeted unregister. Last-registered-wins gives Bible
  verse navigation priority over slide nav while module is mounted.

- Add label field to Hotkeys.list(); HotkeysCheatsheet uses label for
  <kbd> rendering (shows "←" instead of "arrowleft"). Add "bible" group
  to GROUP_ORDER + i18n (pt/es).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove dead code from ModuleManager.js: the `modules` Map, `manifests`
Map, and `register()` method were never called externally or internally —
`modules` was always empty and `manifests` never populated.

Fix latent crash: moduleGroups[category].modules throws TypeError when
a module declares a category not pre-declared in state.js. Added
a dynamic guard that creates the category entry on first use.

Add explicit JSDoc responsibility headers to both files with cross-
references ("does NOT do X — that is Y.js") to make the boundary
unambiguous for future contributors.

No behavior changes; public API (Modules.open/close/get,
ModuleManager.init/installModule) is unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Audit confirms Vuetify 4 VDialog provides focus-trap natively for all
modals in the project: retainFocus:true (Tab cycling trapped inside the
dialog) and captureFocus:true (focus moves to first element on open) are
both Vuetify 4 defaults. VBottomSheet inherits the same defaults via
makeVDialogProps(). No focus-trap-vue dependency needed.

The only gap found: LiturgyItemForm and LiturgySchedules used the
`persistent` prop without an Escape handler, leaving the close button
(Tab + Enter) as the only keyboard path to dismiss them. Added
@keydown.escape="$emit('update:modelValue', false)" matching the same
pattern used in Window.vue. Click-outside behavior remains unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Guard Path.db() against path traversal by validating the key matches
[a-zA-Z0-9_-]+ before building the URL. Guard Path.file() against
'..' segments and absolute URLs (SSRF-style). Critical for the upcoming
Electron phase where louvorja:// resolves to real filesystem paths.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
juanaleixo and others added 30 commits May 6, 2026 14:03
Workflow publica instaladores Windows/macOS/Linux automaticamente como
GitHub Release quando uma tag v*.*.* é pushada. Pre-releases (`v*.*.*-*`,
ex: v1.28.0-preview.1) viram releases marcadas como `prerelease`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A release 1.28.0-preview.1 falhou porque o workflow esperava
artifacts/**/*.zip mas o mac target só produzia DMG. ZIP também é
necessário para auto-update via electron-updater no macOS.
Ribbon contextual:
- Dividido em 4 sub-abas (Arquivo / Slides / Áudio/Gravação / Visualização) com paridade Delphi
- Botões padronizados via token --lj-small-btn-width

Layout:
- Sidebar reorganizada em accordions (Texto, Cor & Tipografia, Imagem, Replicar)
- Player customizado abaixo do preview com timeline e markers dos slides gravados
- Preview usa o mesmo clamp da projeção real (useSlideStyle + cfg global)

ModuleContainer:
- Prop beforeClose para módulos cancelarem fechamento

Bug fixes (slide_editor):
- SljaConverter: encoding windows-1252 (Delphi ANSI) e interop JSZip via Vite
- actProject: abre janela /projection e envia payload completo (cor, tamanho, imagem, posição) com live sync
- actSave/SaveAs: materializa tokens pkg:// → lib:// para preservar mídia ao reabrir
- recordRetroactive grava no slide atual com offset (paridade Delphi)
- Sincronização total: lista, ribbon, player e timeline conversam (goSlide faz seek)
- Feedback \$alert nos botões de áudio sem arquivo anexado
- recordAdvance: avança primeiro e grava no slide novo (paridade Delphi acaoSlide 'prox_grava'), evitando o syncSlideFromAudio puxar de volta
- recordRetroactive: paridade btGravaRClick — primeiro slide faz seek para 0, demais usam o tempo já gravado menos offset e fazem seek no áudio
- goSlide: ao voltar para o slide 1 sempre faz seek para 0 (mesmo sem tempo gravado)
- syncSlideFromAudio: trabalha por delta — só atualiza current quando o tempo cruza um marker em playback contínuo; ignora saltos (seek/navegação manual), permitindo trocar para slides futuros sem tempo
- togglePlay: removido alerta confuso que misturava "Nenhum áudio anexado" com aviso de tempo gravado; agora toca de onde estiver quando o slide não tem tempo
- requireAudio: mensagem ajustada para refletir o que o usuário precisa fazer
Ajuste no css da tela de retorno com safeArea
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant