Compare commits
5 Commits
v0.2.2-dir
...
v0.2.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
545a18cf59 | ||
|
|
7e376f8211 | ||
|
|
c2a17185fd | ||
|
|
b2454f3e9e | ||
|
|
422ff362c3 |
7
Makefile
7
Makefile
@@ -81,7 +81,8 @@ release-all: lint
|
|||||||
@ls -lh dist/*.zip
|
@ls -lh dist/*.zip
|
||||||
|
|
||||||
# Upload release to Gitea (requires GITEA_TOKEN env var)
|
# 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
|
@if [ -z "$(GITEA_TOKEN)" ]; then echo "Error: GITEA_TOKEN not set"; exit 1; fi
|
||||||
@echo "Creating release $(VERSION) on Gitea..."
|
@echo "Creating release $(VERSION) on Gitea..."
|
||||||
@RELEASE_ID=$$(curl -s -X POST \
|
@RELEASE_ID=$$(curl -s -X POST \
|
||||||
@@ -101,8 +102,8 @@ release-upload:
|
|||||||
done
|
done
|
||||||
@echo "Done!"
|
@echo "Done!"
|
||||||
|
|
||||||
# Full release cycle: build + upload
|
# Full release cycle: build + upload (release-upload already depends on release)
|
||||||
release-publish: release release-upload
|
release-publish: release-upload
|
||||||
|
|
||||||
# Help
|
# Help
|
||||||
help:
|
help:
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
import { EventsOn, EventsOff, WindowSetSize, ScreenGetAll } from '../wailsjs/runtime/runtime'
|
import { EventsOn, EventsOff, WindowSetSize, ScreenGetAll } from '../wailsjs/runtime/runtime'
|
||||||
import { GetParticipants, StartMeeting, GetSettings, SkipSpeaker, RemoveFromQueue, SwitchToSpeaker } from '../wailsjs/go/app/App'
|
import { GetParticipants, StartMeeting, GetSettings, SkipSpeaker, RemoveFromQueue, SwitchToSpeaker } from '../wailsjs/go/app/App'
|
||||||
import { t, initLocale } from './lib/i18n'
|
import { t, initLocale } from './lib/i18n'
|
||||||
|
import { attendance } from './lib/stores'
|
||||||
|
|
||||||
let currentView = 'main'
|
let currentView = 'main'
|
||||||
let timerState = null
|
let timerState = null
|
||||||
@@ -222,6 +223,7 @@
|
|||||||
async function loadParticipants() {
|
async function loadParticipants() {
|
||||||
try {
|
try {
|
||||||
participants = await GetParticipants() || []
|
participants = await GetParticipants() || []
|
||||||
|
attendance.init(participants)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to load participants:', e)
|
console.error('Failed to load participants:', e)
|
||||||
participants = []
|
participants = []
|
||||||
@@ -231,12 +233,16 @@
|
|||||||
async function handleQuickStart() {
|
async function handleQuickStart() {
|
||||||
if (participants.length === 0) return
|
if (participants.length === 0) return
|
||||||
|
|
||||||
const ids = participants.map(p => p.id)
|
const att = attendance.get()
|
||||||
const attendance = {}
|
const presentIds = participants.filter(p => att[p.id]).map(p => p.id)
|
||||||
participants.forEach(p => { attendance[p.id] = true })
|
|
||||||
|
if (presentIds.length === 0) {
|
||||||
|
alert($t('setup.noParticipants'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await StartMeeting(ids, attendance)
|
await StartMeeting(presentIds, att)
|
||||||
meetingActive = true
|
meetingActive = true
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to start meeting:', e)
|
console.error('Failed to start meeting:', e)
|
||||||
|
|||||||
@@ -609,12 +609,6 @@
|
|||||||
color: #6b7280;
|
color: #6b7280;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sound-test-buttons {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
margin-top: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.test-btn {
|
.test-btn {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
|
|||||||
@@ -2,13 +2,13 @@
|
|||||||
import { onMount, createEventDispatcher } from 'svelte'
|
import { onMount, createEventDispatcher } from 'svelte'
|
||||||
import { GetParticipants, GetMeeting, StartMeeting, AddParticipant, DeleteParticipant, ReorderParticipants, UpdateParticipant, UpdateMeeting } from '../../wailsjs/go/app/App'
|
import { GetParticipants, GetMeeting, StartMeeting, AddParticipant, DeleteParticipant, ReorderParticipants, UpdateParticipant, UpdateMeeting } from '../../wailsjs/go/app/App'
|
||||||
import { t } from '../lib/i18n'
|
import { t } from '../lib/i18n'
|
||||||
|
import { attendance } from '../lib/stores'
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
let participants = []
|
let participants = []
|
||||||
let meeting = null
|
let meeting = null
|
||||||
let selectedOrder = []
|
let selectedOrder = []
|
||||||
let attendance = {}
|
|
||||||
let loading = true
|
let loading = true
|
||||||
let newName = ''
|
let newName = ''
|
||||||
let newTimeLimitMin = 2
|
let newTimeLimitMin = 2
|
||||||
@@ -37,10 +37,7 @@
|
|||||||
meeting = await GetMeeting()
|
meeting = await GetMeeting()
|
||||||
|
|
||||||
selectedOrder = participants.map(p => p.id)
|
selectedOrder = participants.map(p => p.id)
|
||||||
attendance = {}
|
attendance.init(participants)
|
||||||
participants.forEach(p => {
|
|
||||||
attendance[p.id] = true
|
|
||||||
})
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to load data:', e)
|
console.error('Failed to load data:', e)
|
||||||
}
|
}
|
||||||
@@ -95,8 +92,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function toggleAttendance(id) {
|
function toggleAttendance(id) {
|
||||||
attendance[id] = !attendance[id]
|
attendance.toggle(id)
|
||||||
attendance = attendance
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Drag and drop state
|
// Drag and drop state
|
||||||
@@ -152,14 +148,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleStart() {
|
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) {
|
if (presentIds.length === 0) {
|
||||||
alert($t('setup.noParticipants'))
|
alert($t('setup.noParticipants'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await StartMeeting(presentIds, attendance)
|
await StartMeeting(presentIds, att)
|
||||||
dispatch('started')
|
dispatch('started')
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to start meeting:', e)
|
console.error('Failed to start meeting:', e)
|
||||||
@@ -314,7 +311,7 @@
|
|||||||
{@const p = getParticipant(id)}
|
{@const p = getParticipant(id)}
|
||||||
{#if p}
|
{#if p}
|
||||||
<li
|
<li
|
||||||
class:absent={!attendance[id]}
|
class:absent={!$attendance[id]}
|
||||||
class:drag-over={dragOverId === id}
|
class:drag-over={dragOverId === id}
|
||||||
draggable="true"
|
draggable="true"
|
||||||
on:dragstart={(e) => handleDragStart(e, id)}
|
on:dragstart={(e) => handleDragStart(e, id)}
|
||||||
@@ -329,10 +326,10 @@
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
class="attendance-toggle"
|
class="attendance-toggle"
|
||||||
class:present={attendance[id]}
|
class:present={$attendance[id]}
|
||||||
on:click={() => toggleAttendance(id)}
|
on:click={() => toggleAttendance(id)}
|
||||||
>
|
>
|
||||||
{attendance[id] ? '✓' : '✗'}
|
{$attendance[id] ? '✓' : '✗'}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<span class="name">{p.name}</span>
|
<span class="name">{p.name}</span>
|
||||||
@@ -375,8 +372,8 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="summary">
|
<div class="summary">
|
||||||
<span>{$t('setup.participants')}: {Object.values(attendance).filter(Boolean).length} / {participants.length}</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>
|
<span>≈ {formatTime(selectedOrder.filter(id => $attendance[id]).reduce((acc, id) => acc + (getParticipant(id)?.timeLimit || 0), 0))}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="start-btn" on:click={handleStart}>
|
<button class="start-btn" on:click={handleStart}>
|
||||||
|
|||||||
56
frontend/src/lib/stores.js
Normal file
56
frontend/src/lib/stores.js
Normal 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();
|
||||||
2
frontend/wailsjs/go/app/App.d.ts
vendored
2
frontend/wailsjs/go/app/App.d.ts
vendored
@@ -53,6 +53,8 @@ export function RestartApp():Promise<void>;
|
|||||||
|
|
||||||
export function ResumeMeeting():Promise<void>;
|
export function ResumeMeeting():Promise<void>;
|
||||||
|
|
||||||
|
export function SaveWindowPosition():Promise<void>;
|
||||||
|
|
||||||
export function SelectCustomSound(arg1:string):Promise<string>;
|
export function SelectCustomSound(arg1:string):Promise<string>;
|
||||||
|
|
||||||
export function SkipSpeaker():Promise<void>;
|
export function SkipSpeaker():Promise<void>;
|
||||||
|
|||||||
@@ -102,6 +102,10 @@ export function ResumeMeeting() {
|
|||||||
return window['go']['app']['App']['ResumeMeeting']();
|
return window['go']['app']['App']['ResumeMeeting']();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function SaveWindowPosition() {
|
||||||
|
return window['go']['app']['App']['SaveWindowPosition']();
|
||||||
|
}
|
||||||
|
|
||||||
export function SelectCustomSound(arg1) {
|
export function SelectCustomSound(arg1) {
|
||||||
return window['go']['app']['App']['SelectCustomSound'](arg1);
|
return window['go']['app']['App']['SelectCustomSound'](arg1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -327,6 +327,8 @@ export namespace models {
|
|||||||
theme: string;
|
theme: string;
|
||||||
windowWidth: number;
|
windowWidth: number;
|
||||||
windowFullHeight: boolean;
|
windowFullHeight: boolean;
|
||||||
|
windowX: number;
|
||||||
|
windowY: number;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new Settings(source);
|
return new Settings(source);
|
||||||
@@ -345,6 +347,8 @@ export namespace models {
|
|||||||
this.theme = source["theme"];
|
this.theme = source["theme"];
|
||||||
this.windowWidth = source["windowWidth"];
|
this.windowWidth = source["windowWidth"];
|
||||||
this.windowFullHeight = source["windowFullHeight"];
|
this.windowFullHeight = source["windowFullHeight"];
|
||||||
|
this.windowX = source["windowX"];
|
||||||
|
this.windowY = source["windowY"];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export class SpeakerInfo {
|
export class SpeakerInfo {
|
||||||
|
|||||||
@@ -105,11 +105,16 @@ func (t *Timer) Start() {
|
|||||||
t.speakerWarned = false
|
t.speakerWarned = false
|
||||||
t.meetingWarned = false
|
t.meetingWarned = false
|
||||||
|
|
||||||
if len(t.queue) > 0 {
|
hasSpeakers := len(t.queue) > 0
|
||||||
|
if hasSpeakers {
|
||||||
t.startNextSpeaker(now, 0)
|
t.startNextSpeaker(now, 0)
|
||||||
}
|
}
|
||||||
t.mu.Unlock()
|
t.mu.Unlock()
|
||||||
|
|
||||||
|
if hasSpeakers {
|
||||||
|
t.emit(EventSpeakerChanged)
|
||||||
|
}
|
||||||
|
|
||||||
go t.tick()
|
go t.tick()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user