auto update desktop app
All checks were successful
/ upload (release) Successful in 3m23s

This commit is contained in:
pavel 2026-02-26 23:58:11 +01:00
commit 832de038ca
8 changed files with 526 additions and 9 deletions

View file

@ -112,6 +112,9 @@ const el = {
userName: document.getElementById("user-name"),
userAvatar: document.getElementById("user-avatar"),
logoutBtn: document.getElementById("logout-btn"),
updateNotifier: document.getElementById("update-notifier"),
updateDownloadBtn: document.getElementById("update-download-btn"),
updateInstallBtn: document.getElementById("update-install-btn"),
// Voice Connection
voiceConnection: document.getElementById("voice-connection"),
@ -1940,6 +1943,324 @@ async function init() {
await loadGuilds();
await loadDMConversations();
} catch (err) {
console.error("presence sync failed", err);
}
}
function initUpdater() {
if (!window.electronAPI) return;
window.electronAPI.onUpdateAvailable((info) => {
console.log("Update available:", info.version);
el.updateNotifier.classList.remove("hidden");
el.updateDownloadBtn.classList.remove("hidden");
el.updateInstallBtn.classList.add("hidden");
});
window.electronAPI.onUpdateDownloaded((info) => {
console.log("Update downloaded:", info.version);
el.updateNotifier.classList.remove("hidden");
el.updateDownloadBtn.classList.add("hidden");
el.updateInstallBtn.classList.remove("hidden");
el.updateInstallBtn.title = `Install Update ${info.version}`;
});
window.electronAPI.onUpdateError((err) => {
console.error("Update error:", err);
// Optionally hide indicator on error
// el.updateNotifier.classList.add("hidden");
});
el.updateDownloadBtn.onclick = () => {
el.updateDownloadBtn.disabled = true;
el.updateDownloadBtn.style.opacity = "0.5";
window.electronAPI.downloadUpdate();
};
el.updateInstallBtn.onclick = () => {
window.electronAPI.quitAndInstall();
};
}
async function init() {
lucide.createIcons();
try {
const searchParams = new URLSearchParams(location.search);
const jwtToken = searchParams.get("token");
const refreshToken = searchParams.get("refresh_token");
if (jwtToken || refreshToken) {
if (jwtToken) localStorage.setItem("chattz_token", jwtToken);
if (refreshToken) localStorage.setItem("chattz_refresh_token", refreshToken);
searchParams.delete("token");
searchParams.delete("refresh_token");
const nextQuery = searchParams.toString();
const nextUrl = `${location.pathname}${nextQuery ? `?${nextQuery}` : ""}`;
history.replaceState(null, "", nextUrl);
}
} catch (e) {
console.error("Failed to parse token from URL:", e);
}
el.loginBtn.onclick = () => { location.href = "/auth/login"; };
el.logoutBtn.onclick = async () => {
await leaveVoice();
localStorage.removeItem("chattz_token");
localStorage.removeItem("chattz_refresh_token");
try { await api("/auth/logout", { method: "POST" }); } catch (e) { }
location.reload();
};
el.addGuildBtn.onclick = () => { el.modalContainer.classList.remove("hidden"); };
el.createInviteBtn.onclick = createInviteLink;
el.modalCancel.onclick = () => { el.modalContainer.classList.add("hidden"); };
el.addTextBtn.onclick = () => {
if (!state.selectedGuildId) {
alert("Create or select a server first.");
return;
}
el.modalTitle.textContent = "Create Text Channel";
el.channelModal.classList.remove("hidden");
};
el.addVoiceBtn.onclick = () => {
if (!state.selectedGuildId) {
alert("Create or select a server first.");
return;
}
el.modalTitle.textContent = "Create Voice Channel";
el.channelModal.classList.remove("hidden");
};
el.channelModalCancel.onclick = () => { el.channelModal.classList.add("hidden"); };
el.guildForm.onsubmit = async (e) => {
e.preventDefault();
const name = el.guildName.value.trim();
if (!name) return;
try {
const guild = await api("/guilds", {
method: "POST",
body: JSON.stringify({ name }),
});
el.guildName.value = "";
el.modalContainer.classList.add("hidden");
state.guilds.push(guild);
state.selectedGuildId = guild.id;
state.selectedTextChannelId = null;
state.selectedVoiceChannelId = null;
state.selectedDmUserId = null;
state.selectedDmDisplayName = null;
localStorage.setItem(LAST_GUILD_STORAGE_KEY, guild.id);
renderGuilds();
await loadChannels();
await loadGuildMembers();
await refreshVoicePresence();
renderMessages([]);
updateHeaderLabels();
} catch (err) { alert(err.message); }
};
el.channelForm.onsubmit = async (e) => {
e.preventDefault();
if (!state.selectedGuildId) {
alert("Select a server first.");
return;
}
const kindInput = document.querySelector('input[name="channel-kind"]:checked');
const kind = kindInput ? kindInput.value : "text";
const channelName = el.channelName.value.trim();
if (!channelName) {
alert("Channel name is required.");
return;
}
try {
const created = await api("/channels", {
method: "POST",
body: JSON.stringify({
guild_id: state.selectedGuildId,
name: channelName,
kind: kind,
}),
});
el.channelName.value = "";
el.channelModal.classList.add("hidden");
await loadChannels();
if (created.kind === "text") {
state.selectedTextChannelId = created.id;
state.selectedVoiceChannelId = null;
state.selectedDmUserId = null;
state.selectedDmDisplayName = null;
renderChannels();
renderDMs();
updateHeaderLabels();
await api(`/channels/${created.id}/messages?limit=100`).then(renderMessages);
} else {
state.selectedVoiceChannelId = created.id;
state.selectedTextChannelId = null;
state.selectedDmUserId = null;
state.selectedDmDisplayName = null;
renderChannels();
renderDMs();
updateHeaderLabels();
await joinVoice();
}
} catch (err) { alert(err.message); }
};
el.messageForm.onsubmit = async (e) => {
e.preventDefault();
const body = el.messageBody.value.trim();
if (!body) return;
try {
if (state.selectedTextChannelId) {
await api(`/channels/${state.selectedTextChannelId}/messages`, {
method: "POST",
body: JSON.stringify({ body }),
});
} else if (state.selectedDmUserId) {
await api(`/dms/${state.selectedDmUserId}/messages`, {
method: "POST",
body: JSON.stringify({ body }),
});
} else {
return;
}
el.messageBody.value = "";
const messages = state.selectedTextChannelId
? await api(`/channels/${state.selectedTextChannelId}/messages?limit=100`)
: await api(`/dms/${state.selectedDmUserId}/messages?limit=100`);
renderMessages(messages);
await loadDMConversations();
renderDMs();
} catch (err) { alert(err.message); }
};
el.voiceVideoBtn.onclick = toggleVideo;
el.voiceScreenBtn.onclick = toggleScreenShare;
el.voiceMuteBtn.onclick = toggleMute;
el.voiceLeaveBtn.onclick = leaveVoice;
el.addSoundBtn.onclick = () => {
el.soundModal.classList.remove("hidden");
};
el.soundModalCancel.onclick = () => {
el.soundModal.classList.add("hidden");
};
el.soundForm.onsubmit = async (e) => {
e.preventDefault();
if (!state.selectedGuildId) return;
const formData = new FormData();
formData.append('name', el.soundName.value);
formData.append('icon', el.soundIcon.value);
formData.append('file', el.soundFile.files[0]);
el.soundSubmitBtn.disabled = true;
el.soundSubmitBtn.textContent = 'Uploading...';
try {
// Need to use fetch directly because api() might not handle FormData correctly depending on implementation
const response = await fetch(`/guilds/${state.selectedGuildId}/sounds`, {
method: 'POST',
body: formData
});
if (!response.ok) {
const err = await response.json();
throw new Error(err.error || 'Upload failed');
}
el.soundModal.classList.add("hidden");
el.soundForm.reset();
await loadSounds();
} catch (err) {
alert(err.message);
} finally {
el.soundSubmitBtn.disabled = false;
el.soundSubmitBtn.textContent = 'Add Sound';
}
};
// --- GIF Picker Logic ---
const TENOR_API_KEY = "exTiFGKJ0CzESIHzVQWy3pRO8I1MAdpRomg95DBSu2sg6e7YcHgThMI4giGAx8D0"; // Placeholder / Public key if available, otherwise "YOUR_API_KEY"
const TENOR_CLIENT_KEY = "pavel-discord";
el.gifBtn.onclick = () => {
el.gifPicker.classList.remove("hidden");
searchGifs(""); // Initial featured search
};
el.gifPickerClose.onclick = () => {
el.gifPicker.classList.add("hidden");
};
let searchTimeout = null;
el.gifSearchInput.oninput = () => {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
searchGifs(el.gifSearchInput.value);
}, 500);
};
async function searchGifs(query) {
el.gifResults.innerHTML = '<div class="gif-loading">Searching...</div>';
const baseUrl = "https://api.klipy.com/v2";
const endpoint = query
? `${baseUrl}/search?q=${encodeURIComponent(query)}&key=${TENOR_API_KEY}&client_key=${TENOR_CLIENT_KEY}&limit=20`
: `${baseUrl}/featured?key=${TENOR_API_KEY}&client_key=${TENOR_CLIENT_KEY}&limit=20`;
try {
const res = await fetch(endpoint);
const data = await res.json();
renderGifs(data.results);
} catch (err) {
el.gifResults.innerHTML = '<div class="gif-loading">Error loading GIFs</div>';
}
}
function renderGifs(gifs) {
el.gifResults.innerHTML = "";
if (!gifs || gifs.length === 0) {
el.gifResults.innerHTML = '<div class="gif-loading">No GIFs found</div>';
return;
}
gifs.forEach(gif => {
// Use tinygif for preview, standard gif for sending
const previewUrl = gif.media_formats.tinygif.url;
const fullUrl = gif.media_formats.gif.url;
const item = document.createElement("div");
item.className = "gif-item";
const img = document.createElement("img");
img.src = previewUrl;
img.loading = "lazy";
img.onclick = () => {
el.messageBody.value = fullUrl;
el.gifPicker.classList.add("hidden");
el.messageForm.dispatchEvent(new Event('submit'));
};
item.appendChild(img);
el.gifResults.appendChild(item);
});
}
try {
state.me = await api("/me");
el.userName.textContent = state.me.display_name;
el.userAvatar.textContent = state.me.display_name[0].toUpperCase();
state.guilds = await api("/guilds");
renderGuilds();
await loadDMConversations();
try {
const online = await api("/presence");
@ -1964,6 +2285,7 @@ async function init() {
initChatWs();
startVoicePresencePolling();
initUpdater();
lucide.createIcons();
} catch (err) {
console.error("init failed", err);

View file

@ -114,6 +114,11 @@
<div class="user-status">Online</div>
</div>
<div class="user-actions">
<div id="update-notifier" class="update-notifier hidden">
<button id="update-download-btn" title="Download Update"><i data-lucide="download"></i></button>
<button id="update-install-btn" title="Install Update" class="hidden"><i
data-lucide="arrow-up-circle"></i></button>
</div>
<button id="user-settings-btn" title="Settings"><i data-lucide="settings"></i></button>
<button id="logout-btn" title="Logout"><i data-lucide="log-out"></i></button>
</div>
@ -263,4 +268,4 @@
<script src="app.js?v=20260225-voice-debug-1" defer></script>
</body>
</html>
</html>

View file

@ -2,5 +2,11 @@ const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
copyToClipboard: (text) => ipcRenderer.invoke('clipboard-write', text),
getConfig: () => ipcRenderer.invoke('get-config')
getConfig: () => ipcRenderer.invoke('get-config'),
checkForUpdates: () => ipcRenderer.send('check-for-updates'),
downloadUpdate: () => ipcRenderer.send('download-update'),
quitAndInstall: () => ipcRenderer.send('quit-and-install'),
onUpdateAvailable: (callback) => ipcRenderer.on('update-available', (event, info) => callback(info)),
onUpdateDownloaded: (callback) => ipcRenderer.on('update-downloaded', (event, info) => callback(info)),
onUpdateError: (callback) => ipcRenderer.on('update-error', (event, error) => callback(error))
});

View file

@ -621,6 +621,35 @@ select {
box-shadow: 0 0 4px var(--brand-glow);
}
.update-notifier {
display: flex;
align-items: center;
margin-right: 8px;
}
.update-notifier button {
background: none;
border: none;
padding: 4px;
color: var(--brand);
cursor: pointer;
transition: transform 0.2s;
display: flex;
align-items: center;
justify-content: center;
}
.update-notifier button:hover {
transform: scale(1.2);
color: var(--brand-glow);
}
.update-notifier button i {
width: 16px;
height: 16px;
}
.user-info {
flex: 1;
min-width: 0;