add support for directly launching windows exes from file exporer using .desktop shortcut
This commit is contained in:
parent
66f42f30cc
commit
5e217472aa
15 changed files with 440 additions and 28 deletions
109
renderer/app.js
109
renderer/app.js
|
|
@ -597,6 +597,114 @@ async function doImport(srcDir, mode) {
|
|||
}
|
||||
}
|
||||
|
||||
// --- "Open with PLWL" (file manager -> Open With -> PLWL) -----------------------
|
||||
|
||||
const openwithTexts = {}
|
||||
|
||||
function ot(key, ...args) {
|
||||
let text = openwithTexts[key] ?? t(key)
|
||||
args.forEach((v, i) => {
|
||||
text = String(text).split(`{${i}}`).join(String(v))
|
||||
})
|
||||
return text
|
||||
}
|
||||
|
||||
async function loadOpenwithTexts() {
|
||||
try {
|
||||
Object.assign(openwithTexts, await window.plwl.openwithTexts(state.lang))
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function isWindowsExecutable(p) {
|
||||
return /\.(exe|msi|bat|cmd|lnk)$/i.test(p || '')
|
||||
}
|
||||
|
||||
// The popup: wine version + prefix selects defaulting to the LAUNCHER's
|
||||
// defaults (settings.defaultWine / prefixes.default), like the per-app
|
||||
// config's "Default (...)" options - just without importing anything.
|
||||
async function showOpenWithModal(openPath) {
|
||||
// always fresh: wines/prefixes/default can change while the launcher sits
|
||||
// in the tray
|
||||
try {
|
||||
const [wines, prefixes, settings] = await Promise.all([
|
||||
window.plwl.winesList(),
|
||||
window.plwl.prefixesList(),
|
||||
window.plwl.settingsGet()
|
||||
])
|
||||
state.wines = wines
|
||||
state.prefixes = prefixes
|
||||
state.settings = settings
|
||||
} catch {}
|
||||
const modal = showModal(`
|
||||
<h2>${ot('openwith.title')}</h2>
|
||||
<p class="subtitle openwith-file-row">${ot('openwith.file')}: <code id="ow-file"></code></p>
|
||||
<p class="subtitle" id="ow-missing" style="display:none">${ot('openwith.fileMissing')}</p>
|
||||
<label class="field-label">${ot('openwith.wine')}</label>
|
||||
<span id="ow-wine"></span>
|
||||
<label class="field-label">${ot('openwith.prefix')}</label>
|
||||
<span id="ow-prefix"></span>
|
||||
<div class="modal-actions">
|
||||
<button class="button" id="ow-cancel">${t('dialog.cancel')}</button>
|
||||
<button class="button primary" id="ow-run">${ot('openwith.run')}</button>
|
||||
</div>
|
||||
`)
|
||||
modal.querySelector('#ow-file').textContent = openPath
|
||||
modal.querySelector('#ow-file').title = openPath
|
||||
const missing = !window.plwl.fileExists(openPath) || !isWindowsExecutable(openPath)
|
||||
modal.querySelector('#ow-missing').style.display = missing ? '' : 'none'
|
||||
const defWine = (state.settings && state.settings.defaultWine) || 'system'
|
||||
const defPrefix = state.prefixes.default || ''
|
||||
const wineOpts = [
|
||||
{ value: 'default', label: ot('openwith.wineDefault', wineLabel(defWine)) },
|
||||
...state.wines.map(w => ({ value: w.id, label: w.label }))
|
||||
]
|
||||
const prefixOpts = [
|
||||
{ value: '', label: ot('openwith.prefixDefault', defPrefix || 'Main') },
|
||||
...state.prefixes.prefixes.filter(p => p.name !== defPrefix).map(p => ({ value: p.name, label: p.name }))
|
||||
]
|
||||
new CustomSelect(modal.querySelector('#ow-wine'), wineOpts, 'default')
|
||||
new CustomSelect(modal.querySelector('#ow-prefix'), prefixOpts, '')
|
||||
modal.querySelector('#ow-cancel').onclick = closeModal
|
||||
modal.querySelector('#ow-run').onclick = async () => {
|
||||
if (missing) {
|
||||
closeModal()
|
||||
return
|
||||
}
|
||||
const runBtn = modal.querySelector('#ow-run')
|
||||
runBtn.disabled = true
|
||||
const res = await window.plwl.openwithRun({
|
||||
openPath,
|
||||
wine: modal.querySelector('#ow-wine').dataset.value || 'default',
|
||||
prefix: modal.querySelector('#ow-prefix').dataset.value || ''
|
||||
})
|
||||
closeModal()
|
||||
if (res.ok) {
|
||||
toast(t('toast.started', window.plwl.basename(openPath)), 'success')
|
||||
} else {
|
||||
const raw = res.error === 'file-missing' ? ot('openwith.fileMissing') : (res.error === 'not-windows' ? ot('openwith.notWindows') : (res.error || ''))
|
||||
toast(ot('openwith.startFailed', window.plwl.basename(openPath), raw), 'error')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Entry point: the main process hands over a file opened through PLWL
|
||||
// (boot argv or a second-instance handoff).
|
||||
async function handleOpenWith(openPath) {
|
||||
if (!openPath) return
|
||||
await loadOpenwithTexts()
|
||||
showOpenWithModal(openPath)
|
||||
}
|
||||
|
||||
function wireOpenWith() {
|
||||
// Both delivery paths (live handoff and the boot queue) converge on
|
||||
// openwithTake(): the main-process slot is drained exactly when a popup
|
||||
// shows, so a reloaded window can never resurrect an old popup and two
|
||||
// quick handoffs collapse into one (the newest file wins).
|
||||
window.plwl.onOpenWith(() => window.plwl.openwithTake().then(handleOpenWith))
|
||||
// file queued before the window existed (boot handoff): pick it up once
|
||||
window.plwl.openwithTake().then(handleOpenWith)
|
||||
}
|
||||
|
||||
// Import from a prefix, step 2: register the app in place - nothing is
|
||||
// copied, paths stay prefix-relative.
|
||||
async function doPrefixImport(prefix, srcDir) {
|
||||
|
|
@ -1075,6 +1183,7 @@ async function boot() {
|
|||
applyI18n()
|
||||
wireEvents()
|
||||
wireRealtime()
|
||||
wireOpenWith()
|
||||
qs('#loading').remove()
|
||||
qs('#main').style.display = 'flex'
|
||||
showView('library')
|
||||
|
|
|
|||
|
|
@ -627,6 +627,36 @@ select {
|
|||
.modal .mode-buttons { display: flex; flex-direction: column; gap: 0.6rem; }
|
||||
.modal .mode-buttons .button { justify-content: flex-start; }
|
||||
|
||||
/* --- "Open with PLWL" picker ---------------------------------------------- */
|
||||
.modal .openwith-file-row {
|
||||
word-break: break-all;
|
||||
margin-top: -0.3rem;
|
||||
}
|
||||
.modal .openwith-file-row code {
|
||||
font-family: inherit;
|
||||
color: var(--text-color);
|
||||
background-color: rgba(255, 255, 255, 0.06);
|
||||
border-radius: 0.4rem;
|
||||
padding: 0.1rem 0.4rem;
|
||||
}
|
||||
.button:disabled {
|
||||
opacity: 0.45;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* --- form fields inside modals (shared by every modal with inputs) --------- */
|
||||
/* .modal is a flex column with gap: 0.8rem. That gap ADDS to the field
|
||||
margins reused from the config forms (.field-label margin-top + control
|
||||
margin), so label->control spacing in modals ended up ~3x bigger than in
|
||||
.form-card (where block-layout margins collapse). Inside modals the gap
|
||||
owns the group spacing, so the repeated field styles collapse to the same
|
||||
rhythm as the config forms: label hugs its control (~0.4rem), groups stay
|
||||
0.8rem apart. */
|
||||
.modal .field-label { margin-top: 0; margin-bottom: -0.4rem; }
|
||||
.modal .custom-select,
|
||||
.modal input,
|
||||
.modal textarea { margin: 0; }
|
||||
|
||||
.toasts {
|
||||
position: fixed;
|
||||
bottom: 1rem;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue