5 Commits

Author SHA1 Message Date
Mikhail Kiselev
545a18cf59 feat: global attendance store persists between views 2026-02-11 00:10:04 +03:00
Mikhail Kiselev
7e376f8211 fix: release-upload depends on release target 2026-02-10 23:58:51 +03:00
Mikhail Kiselev
c2a17185fd fix: save first speaker log on meeting start 2026-02-10 23:54:23 +03:00
Mikhail Kiselev
b2454f3e9e chore: remove unused CSS selector 2026-02-10 23:43:46 +03:00
Mikhail Kiselev
422ff362c3 chore: update wails bindings 2026-02-10 23:39:02 +03:00
9 changed files with 97 additions and 28 deletions

View File

@@ -81,7 +81,8 @@ release-all: lint
@ls -lh dist/*.zip
# Upload release to Gitea (requires GITEA_TOKEN env var)
release-upload:
# Depends on 'release' to ensure dist/ files are up-to-date
release-upload: release
@if [ -z "$(GITEA_TOKEN)" ]; then echo "Error: GITEA_TOKEN not set"; exit 1; fi
@echo "Creating release $(VERSION) on Gitea..."
@RELEASE_ID=$$(curl -s -X POST \
@@ -101,8 +102,8 @@ release-upload:
done
@echo "Done!"
# Full release cycle: build + upload
release-publish: release release-upload
# Full release cycle: build + upload (release-upload already depends on release)
release-publish: release-upload
# Help
help:

View File

@@ -9,6 +9,7 @@
import { EventsOn, EventsOff, WindowSetSize, ScreenGetAll } from '../wailsjs/runtime/runtime'
import { GetParticipants, StartMeeting, GetSettings, SkipSpeaker, RemoveFromQueue, SwitchToSpeaker } from '../wailsjs/go/app/App'
import { t, initLocale } from './lib/i18n'
import { attendance } from './lib/stores'
let currentView = 'main'
let timerState = null
@@ -222,6 +223,7 @@
async function loadParticipants() {
try {
participants = await GetParticipants() || []
attendance.init(participants)
} catch (e) {
console.error('Failed to load participants:', e)
participants = []
@@ -231,12 +233,16 @@
async function handleQuickStart() {
if (participants.length === 0) return
const ids = participants.map(p => p.id)
const attendance = {}
participants.forEach(p => { attendance[p.id] = true })
const att = attendance.get()
const presentIds = participants.filter(p => att[p.id]).map(p => p.id)
if (presentIds.length === 0) {
alert($t('setup.noParticipants'))
return
}
try {
await StartMeeting(ids, attendance)
await StartMeeting(presentIds, att)
meetingActive = true
} catch (e) {
console.error('Failed to start meeting:', e)

View File

@@ -609,12 +609,6 @@
color: #6b7280;
}
.sound-test-buttons {
display: flex;
gap: 8px;
margin-top: 12px;
}
.test-btn {
flex: 1;
padding: 10px 12px;

View File

@@ -2,13 +2,13 @@
import { onMount, createEventDispatcher } from 'svelte'
import { GetParticipants, GetMeeting, StartMeeting, AddParticipant, DeleteParticipant, ReorderParticipants, UpdateParticipant, UpdateMeeting } from '../../wailsjs/go/app/App'
import { t } from '../lib/i18n'
import { attendance } from '../lib/stores'
const dispatch = createEventDispatcher()
let participants = []
let meeting = null
let selectedOrder = []
let attendance = {}
let loading = true
let newName = ''
let newTimeLimitMin = 2
@@ -37,10 +37,7 @@
meeting = await GetMeeting()
selectedOrder = participants.map(p => p.id)
attendance = {}
participants.forEach(p => {
attendance[p.id] = true
})
attendance.init(participants)
} catch (e) {
console.error('Failed to load data:', e)
}
@@ -95,8 +92,7 @@
}
function toggleAttendance(id) {
attendance[id] = !attendance[id]
attendance = attendance
attendance.toggle(id)
}
// Drag and drop state
@@ -152,14 +148,15 @@
}
async function handleStart() {
const presentIds = selectedOrder.filter(id => attendance[id])
const att = attendance.get()
const presentIds = selectedOrder.filter(id => att[id])
if (presentIds.length === 0) {
alert($t('setup.noParticipants'))
return
}
try {
await StartMeeting(presentIds, attendance)
await StartMeeting(presentIds, att)
dispatch('started')
} catch (e) {
console.error('Failed to start meeting:', e)
@@ -314,7 +311,7 @@
{@const p = getParticipant(id)}
{#if p}
<li
class:absent={!attendance[id]}
class:absent={!$attendance[id]}
class:drag-over={dragOverId === id}
draggable="true"
on:dragstart={(e) => handleDragStart(e, id)}
@@ -329,10 +326,10 @@
<button
class="attendance-toggle"
class:present={attendance[id]}
class:present={$attendance[id]}
on:click={() => toggleAttendance(id)}
>
{attendance[id] ? '✓' : '✗'}
{$attendance[id] ? '✓' : '✗'}
</button>
<span class="name">{p.name}</span>
@@ -375,8 +372,8 @@
{/if}
<div class="summary">
<span>{$t('setup.participants')}: {Object.values(attendance).filter(Boolean).length} / {participants.length}</span>
<span>{formatTime(selectedOrder.filter(id => attendance[id]).reduce((acc, id) => acc + (getParticipant(id)?.timeLimit || 0), 0))}</span>
<span>{$t('setup.participants')}: {Object.values($attendance).filter(Boolean).length} / {participants.length}</span>
<span>{formatTime(selectedOrder.filter(id => $attendance[id]).reduce((acc, id) => acc + (getParticipant(id)?.timeLimit || 0), 0))}</span>
</div>
<button class="start-btn" on:click={handleStart}>

View File

@@ -0,0 +1,56 @@
import { writable, get } from 'svelte/store';
function createAttendanceStore() {
const { subscribe, set, update } = writable({});
return {
subscribe,
// Initialize attendance for all participants (default: true)
init(participants) {
const current = get({ subscribe });
const newAttendance = {};
for (const p of participants) {
// Keep existing value or default to true
newAttendance[p.id] = current[p.id] !== undefined ? current[p.id] : true;
}
set(newAttendance);
},
// Toggle attendance for a participant
toggle(id) {
update((att) => {
att[id] = !att[id];
return { ...att };
});
},
// Set attendance for a participant
set(id, present) {
update((att) => {
att[id] = present;
return { ...att };
});
},
// Get current attendance object
get() {
return get({ subscribe });
},
// Reset all to true
resetAll() {
update((att) => {
const reset = {};
for (const id in att) {
reset[id] = true;
}
return reset;
});
},
};
}
export const attendance = createAttendanceStore();

View File

@@ -53,6 +53,8 @@ export function RestartApp():Promise<void>;
export function ResumeMeeting():Promise<void>;
export function SaveWindowPosition():Promise<void>;
export function SelectCustomSound(arg1:string):Promise<string>;
export function SkipSpeaker():Promise<void>;

View File

@@ -102,6 +102,10 @@ export function ResumeMeeting() {
return window['go']['app']['App']['ResumeMeeting']();
}
export function SaveWindowPosition() {
return window['go']['app']['App']['SaveWindowPosition']();
}
export function SelectCustomSound(arg1) {
return window['go']['app']['App']['SelectCustomSound'](arg1);
}

View File

@@ -327,6 +327,8 @@ export namespace models {
theme: string;
windowWidth: number;
windowFullHeight: boolean;
windowX: number;
windowY: number;
static createFrom(source: any = {}) {
return new Settings(source);
@@ -345,6 +347,8 @@ export namespace models {
this.theme = source["theme"];
this.windowWidth = source["windowWidth"];
this.windowFullHeight = source["windowFullHeight"];
this.windowX = source["windowX"];
this.windowY = source["windowY"];
}
}
export class SpeakerInfo {

View File

@@ -105,11 +105,16 @@ func (t *Timer) Start() {
t.speakerWarned = false
t.meetingWarned = false
if len(t.queue) > 0 {
hasSpeakers := len(t.queue) > 0
if hasSpeakers {
t.startNextSpeaker(now, 0)
}
t.mu.Unlock()
if hasSpeakers {
t.emit(EventSpeakerChanged)
}
go t.tick()
}