Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
41bc35fd43 | ||
|
|
79214910f1 | ||
|
|
9b65c95000 | ||
|
|
577b1abe9b |
@@ -7,7 +7,7 @@
|
|||||||
import History from './components/History.svelte'
|
import History from './components/History.svelte'
|
||||||
import Setup from './components/Setup.svelte'
|
import Setup from './components/Setup.svelte'
|
||||||
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, OpenBrowserURL } from '../wailsjs/go/app/App'
|
||||||
import { t, initLocale } from './lib/i18n'
|
import { t, initLocale } from './lib/i18n'
|
||||||
import { attendance } from './lib/stores'
|
import { attendance } from './lib/stores'
|
||||||
|
|
||||||
@@ -36,6 +36,7 @@
|
|||||||
EventsOn('timer:meeting_warning', handleMeetingWarning)
|
EventsOn('timer:meeting_warning', handleMeetingWarning)
|
||||||
EventsOn('timer:meeting_ended', handleMeetingEnded)
|
EventsOn('timer:meeting_ended', handleMeetingEnded)
|
||||||
EventsOn('timer:speaker_changed', handleSpeakerChanged)
|
EventsOn('timer:speaker_changed', handleSpeakerChanged)
|
||||||
|
EventsOn('jira:open', (url) => { if (url) OpenBrowserURL(url) })
|
||||||
|
|
||||||
// Warm up AudioContext on first user interaction
|
// Warm up AudioContext on first user interaction
|
||||||
const warmUpAudio = async () => {
|
const warmUpAudio = async () => {
|
||||||
@@ -96,6 +97,7 @@
|
|||||||
EventsOff('timer:meeting_warning')
|
EventsOff('timer:meeting_warning')
|
||||||
EventsOff('timer:meeting_ended')
|
EventsOff('timer:meeting_ended')
|
||||||
EventsOff('timer:speaker_changed')
|
EventsOff('timer:speaker_changed')
|
||||||
|
EventsOff('jira:open')
|
||||||
})
|
})
|
||||||
|
|
||||||
function handleTimerEvent(state) {
|
function handleTimerEvent(state) {
|
||||||
@@ -290,7 +292,7 @@
|
|||||||
{#if currentView === 'main'}
|
{#if currentView === 'main'}
|
||||||
{#if meetingActive && timerState}
|
{#if meetingActive && timerState}
|
||||||
<div class="timer-view">
|
<div class="timer-view">
|
||||||
<Timer {timerState} />
|
<Timer {timerState} jiraUrl={settings?.defaultJiraUrl || ''} />
|
||||||
<ParticipantList {timerState} on:skip={handleSkipFromList} on:switch={handleSwitchSpeaker} />
|
<ParticipantList {timerState} on:skip={handleSkipFromList} on:switch={handleSwitchSpeaker} />
|
||||||
<Controls {timerState} on:stop={() => meetingActive = false} />
|
<Controls {timerState} on:stop={() => meetingActive = false} />
|
||||||
</div>
|
</div>
|
||||||
@@ -305,6 +307,9 @@
|
|||||||
<button class="secondary-btn" on:click={() => currentView = 'setup'}>
|
<button class="secondary-btn" on:click={() => currentView = 'setup'}>
|
||||||
{$t('timer.editParticipants')}
|
{$t('timer.editParticipants')}
|
||||||
</button>
|
</button>
|
||||||
|
{#if settings?.defaultJiraUrl}
|
||||||
|
<button class="secondary-btn" on:click={() => OpenBrowserURL(settings.defaultJiraUrl)}>🔗 Jira</button>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="no-meeting">
|
<div class="no-meeting">
|
||||||
|
|||||||
@@ -6,8 +6,9 @@
|
|||||||
let sessions = []
|
let sessions = []
|
||||||
let stats = null
|
let stats = null
|
||||||
let loading = true
|
let loading = true
|
||||||
let dateFrom = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0]
|
const _now = new Date()
|
||||||
let dateTo = new Date().toISOString().split('T')[0]
|
let dateFrom = `${_now.getFullYear()}-${String(_now.getMonth() + 1).padStart(2, '0')}-01`
|
||||||
|
let dateTo = `${_now.getFullYear()}-${String(_now.getMonth() + 1).padStart(2, '0')}-${String(_now.getDate()).padStart(2, '0')}`
|
||||||
let exporting = false
|
let exporting = false
|
||||||
let showDeleteAllConfirm = false
|
let showDeleteAllConfirm = false
|
||||||
let deletingSessionId = null
|
let deletingSessionId = null
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script>
|
<script>
|
||||||
import { onMount, createEventDispatcher } from 'svelte'
|
import { onMount, createEventDispatcher } from 'svelte'
|
||||||
import { GetParticipants, GetMeeting, StartMeeting, AddParticipant, DeleteParticipant, ReorderParticipants, UpdateParticipant, UpdateMeeting, GetSettings } from '../../wailsjs/go/app/App'
|
import { GetParticipants, GetMeeting, StartMeeting, AddParticipant, DeleteParticipant, ReorderParticipants, UpdateParticipant, UpdateMeeting, GetSettings, OpenBrowserURL } from '../../wailsjs/go/app/App'
|
||||||
import { t } from '../lib/i18n'
|
import { t } from '../lib/i18n'
|
||||||
import { attendance } from '../lib/stores'
|
import { attendance } from '../lib/stores'
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
<script>
|
<script>
|
||||||
import { onMount, onDestroy } from 'svelte'
|
import { onMount, onDestroy } from 'svelte'
|
||||||
import { t } from '../lib/i18n'
|
import { t } from '../lib/i18n'
|
||||||
|
import { OpenBrowserURL } from '../../wailsjs/go/app/App'
|
||||||
|
|
||||||
export let timerState
|
export let timerState
|
||||||
|
export let jiraUrl = ''
|
||||||
|
|
||||||
let currentTime = ''
|
let currentTime = ''
|
||||||
let clockInterval
|
let clockInterval
|
||||||
@@ -63,6 +65,9 @@
|
|||||||
<div class="timer-container" class:warning={timerState?.warning} class:overtime={timerState?.speakerOvertime}>
|
<div class="timer-container" class:warning={timerState?.warning} class:overtime={timerState?.speakerOvertime}>
|
||||||
<div class="header-row">
|
<div class="header-row">
|
||||||
<div class="current-clock">{currentTime}</div>
|
<div class="current-clock">{currentTime}</div>
|
||||||
|
{#if jiraUrl}
|
||||||
|
<button class="jira-btn" on:click={() => OpenBrowserURL(jiraUrl)} title="Открыть Jira">🔗</button>
|
||||||
|
{/if}
|
||||||
<div class="help-icon">
|
<div class="help-icon">
|
||||||
?
|
?
|
||||||
<div class="help-tooltip">
|
<div class="help-tooltip">
|
||||||
@@ -204,6 +209,17 @@
|
|||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.jira-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
|
padding: 2px 4px;
|
||||||
|
opacity: 0.6;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
.jira-btn:hover { opacity: 1; }
|
||||||
|
|
||||||
.current-clock {
|
.current-clock {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #8899a6;
|
color: #8899a6;
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export const translations = {
|
|||||||
noSpeaker: 'Нет спикера',
|
noSpeaker: 'Нет спикера',
|
||||||
noActiveMeeting: 'Собрание не запущено',
|
noActiveMeeting: 'Собрание не запущено',
|
||||||
goToParticipants: 'Перейдите в раздел Участники, чтобы добавить участников',
|
goToParticipants: 'Перейдите в раздел Участники, чтобы добавить участников',
|
||||||
readyToStart: 'Всё готово к началу',
|
readyToStart: 'Всё готово к началу собрания',
|
||||||
editParticipants: 'Редактировать участников',
|
editParticipants: 'Редактировать участников',
|
||||||
noParticipants: 'Участники не добавлены',
|
noParticipants: 'Участники не добавлены',
|
||||||
registeredParticipants: 'Зарегистрированные участники',
|
registeredParticipants: 'Зарегистрированные участники',
|
||||||
@@ -223,7 +223,7 @@ export const translations = {
|
|||||||
noSpeaker: 'No speaker',
|
noSpeaker: 'No speaker',
|
||||||
noActiveMeeting: 'No active meeting',
|
noActiveMeeting: 'No active meeting',
|
||||||
goToParticipants: 'Go to Participants to add participants',
|
goToParticipants: 'Go to Participants to add participants',
|
||||||
readyToStart: 'Ready to start',
|
readyToStart: 'Ready to start the meeting',
|
||||||
editParticipants: 'Edit participants',
|
editParticipants: 'Edit participants',
|
||||||
noParticipants: 'No participants added',
|
noParticipants: 'No participants added',
|
||||||
registeredParticipants: 'Registered participants',
|
registeredParticipants: 'Registered participants',
|
||||||
|
|||||||
2
frontend/wailsjs/go/app/App.d.ts
vendored
2
frontend/wailsjs/go/app/App.d.ts
vendored
@@ -43,6 +43,8 @@ export function GetVersion():Promise<string>;
|
|||||||
|
|
||||||
export function NextSpeaker():Promise<void>;
|
export function NextSpeaker():Promise<void>;
|
||||||
|
|
||||||
|
export function OpenBrowserURL(arg1:string):Promise<void>;
|
||||||
|
|
||||||
export function PauseMeeting():Promise<void>;
|
export function PauseMeeting():Promise<void>;
|
||||||
|
|
||||||
export function RemoveFromQueue(arg1:number):Promise<void>;
|
export function RemoveFromQueue(arg1:number):Promise<void>;
|
||||||
|
|||||||
@@ -82,6 +82,10 @@ export function NextSpeaker() {
|
|||||||
return window['go']['app']['App']['NextSpeaker']();
|
return window['go']['app']['App']['NextSpeaker']();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function OpenBrowserURL(arg1) {
|
||||||
|
return window['go']['app']['App']['OpenBrowserURL'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
export function PauseMeeting() {
|
export function PauseMeeting() {
|
||||||
return window['go']['app']['App']['PauseMeeting']();
|
return window['go']['app']['App']['PauseMeeting']();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"daily-timer/internal/models"
|
"daily-timer/internal/models"
|
||||||
|
"daily-timer/internal/relay"
|
||||||
"daily-timer/internal/services/updater"
|
"daily-timer/internal/services/updater"
|
||||||
"daily-timer/internal/storage"
|
"daily-timer/internal/storage"
|
||||||
"daily-timer/internal/timer"
|
"daily-timer/internal/timer"
|
||||||
@@ -24,6 +25,8 @@ type App struct {
|
|||||||
session *models.MeetingSession
|
session *models.MeetingSession
|
||||||
currentLogs map[uint]*models.ParticipantLog
|
currentLogs map[uint]*models.ParticipantLog
|
||||||
participantURLs map[uint]string
|
participantURLs map[uint]string
|
||||||
|
jiraBaseURL string
|
||||||
|
relay *relay.Server
|
||||||
updater *updater.Updater
|
updater *updater.Updater
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,12 +34,20 @@ func New(store *storage.Storage) *App {
|
|||||||
return &App{
|
return &App{
|
||||||
store: store,
|
store: store,
|
||||||
currentLogs: make(map[uint]*models.ParticipantLog),
|
currentLogs: make(map[uint]*models.ParticipantLog),
|
||||||
|
relay: relay.New(),
|
||||||
updater: updater.New(),
|
updater: updater.New(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) Startup(ctx context.Context) {
|
func (a *App) Startup(ctx context.Context) {
|
||||||
a.ctx = ctx
|
a.ctx = ctx
|
||||||
|
a.relay.Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) OpenBrowserURL(url string) {
|
||||||
|
if url != "" {
|
||||||
|
runtime.BrowserOpenURL(a.ctx, url)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) OnDomReady(ctx context.Context) {
|
func (a *App) OnDomReady(ctx context.Context) {
|
||||||
@@ -185,23 +196,39 @@ func (a *App) StartMeeting(participantOrder []uint, attendance map[uint]bool) er
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use meeting-specific URL, fall back to default from settings
|
||||||
|
jiraURL := meeting.JiraURL
|
||||||
|
if jiraURL == "" && settings != nil {
|
||||||
|
jiraURL = settings.DefaultJiraUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
a.jiraBaseURL = jiraURL
|
||||||
a.participantURLs = make(map[uint]string)
|
a.participantURLs = make(map[uint]string)
|
||||||
if meeting.JiraURL != "" {
|
if jiraURL != "" {
|
||||||
for _, p := range participants {
|
for _, p := range participants {
|
||||||
url := meeting.JiraURL
|
url := jiraURL
|
||||||
if p.JiraFilter != "" {
|
if p.JiraFilter != "" {
|
||||||
url = meeting.JiraURL + "&quickFilter=" + p.JiraFilter
|
url = jiraURL + "&quickFilter=" + p.JiraFilter
|
||||||
}
|
}
|
||||||
a.participantURLs[p.ID] = url
|
a.participantURLs[p.ID] = url
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if jiraURL != "" {
|
||||||
|
a.relay.Broadcast(jiraURL)
|
||||||
|
go runtime.BrowserOpenURL(a.ctx, fmt.Sprintf("http://127.0.0.1:%d/", relay.Port))
|
||||||
|
}
|
||||||
|
|
||||||
a.timer = timer.New(meeting.TimeLimit, warningThreshold)
|
a.timer = timer.New(meeting.TimeLimit, warningThreshold)
|
||||||
a.timer.SetQueue(queue)
|
a.timer.SetQueue(queue)
|
||||||
a.currentLogs = make(map[uint]*models.ParticipantLog)
|
a.currentLogs = make(map[uint]*models.ParticipantLog)
|
||||||
go a.handleTimerEvents()
|
go a.handleTimerEvents()
|
||||||
a.timer.Start()
|
a.timer.Start()
|
||||||
|
|
||||||
|
if jiraURL != "" {
|
||||||
|
go runtime.BrowserOpenURL(a.ctx, jiraURL)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,7 +248,11 @@ func (a *App) handleTimerEvents() {
|
|||||||
case timer.EventSpeakerChanged:
|
case timer.EventSpeakerChanged:
|
||||||
a.saveSpeakerLog(event.State)
|
a.saveSpeakerLog(event.State)
|
||||||
if url, ok := a.participantURLs[event.State.CurrentSpeakerID]; ok && url != "" {
|
if url, ok := a.participantURLs[event.State.CurrentSpeakerID]; ok && url != "" {
|
||||||
runtime.BrowserOpenURL(a.ctx, url)
|
a.relay.Broadcast(url)
|
||||||
|
runtime.EventsEmit(a.ctx, "jira:open", url)
|
||||||
|
} else if a.jiraBaseURL != "" {
|
||||||
|
a.relay.Broadcast(a.jiraBaseURL)
|
||||||
|
runtime.EventsEmit(a.ctx, "jira:open", a.jiraBaseURL)
|
||||||
}
|
}
|
||||||
runtime.EventsEmit(a.ctx, "timer:speaker_changed", event.State)
|
runtime.EventsEmit(a.ctx, "timer:speaker_changed", event.State)
|
||||||
case timer.EventMeetingEnded:
|
case timer.EventMeetingEnded:
|
||||||
|
|||||||
@@ -79,6 +79,9 @@ func (s *Storage) ensureDefaults() error {
|
|||||||
// Migrate old default value to new default (60 min instead of 15)
|
// Migrate old default value to new default (60 min instead of 15)
|
||||||
s.db.Model(&settings).Update("default_meeting_time", 3600)
|
s.db.Model(&settings).Update("default_meeting_time", 3600)
|
||||||
}
|
}
|
||||||
|
if settings.DefaultJiraUrl == "" {
|
||||||
|
s.db.Model(&settings).Update("default_jira_url", "https://jira.ncloudtech.ru/secure/RapidBoard.jspa?rapidView=1431&projectKey=RNDIN")
|
||||||
|
}
|
||||||
|
|
||||||
var meeting models.Meeting
|
var meeting models.Meeting
|
||||||
result = s.db.First(&meeting)
|
result = s.db.First(&meeting)
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Event struct {
|
type Event struct {
|
||||||
Type EventType `json:"type"`
|
Type EventType `json:"type"`
|
||||||
State models.TimerState `json:"state"`
|
State models.TimerState `json:"state"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,29 +31,29 @@ type Timer struct {
|
|||||||
running bool
|
running bool
|
||||||
paused bool
|
paused bool
|
||||||
|
|
||||||
meetingStartTime time.Time
|
meetingStartTime time.Time
|
||||||
meetingElapsed time.Duration
|
meetingElapsed time.Duration
|
||||||
meetingLimit time.Duration
|
meetingLimit time.Duration
|
||||||
|
|
||||||
speakerStartTime time.Time
|
speakerStartTime time.Time
|
||||||
speakerElapsed time.Duration
|
speakerElapsed time.Duration
|
||||||
speakerLimit time.Duration
|
speakerLimit time.Duration
|
||||||
|
|
||||||
currentSpeakerID uint
|
currentSpeakerID uint
|
||||||
currentSpeaker string
|
currentSpeaker string
|
||||||
speakingOrder int
|
speakingOrder int
|
||||||
queue []models.QueuedSpeaker
|
queue []models.QueuedSpeaker
|
||||||
allSpeakers []models.SpeakerInfo
|
allSpeakers []models.SpeakerInfo
|
||||||
|
|
||||||
warningThreshold time.Duration
|
warningThreshold time.Duration
|
||||||
speakerWarned bool
|
speakerWarned bool
|
||||||
speakerTimeUpEmitted bool
|
speakerTimeUpEmitted bool
|
||||||
meetingWarned bool
|
meetingWarned bool
|
||||||
|
|
||||||
eventCh chan Event
|
eventCh chan Event
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
pausedAt time.Time
|
pausedAt time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(meetingLimitSec, warningThresholdSec int) *Timer {
|
func New(meetingLimitSec, warningThresholdSec int) *Timer {
|
||||||
@@ -105,16 +105,8 @@ func (t *Timer) Start() {
|
|||||||
t.speakerWarned = false
|
t.speakerWarned = false
|
||||||
t.meetingWarned = false
|
t.meetingWarned = false
|
||||||
|
|
||||||
hasSpeakers := len(t.queue) > 0
|
|
||||||
if hasSpeakers {
|
|
||||||
t.startNextSpeaker(now, 0)
|
|
||||||
}
|
|
||||||
t.mu.Unlock()
|
t.mu.Unlock()
|
||||||
|
// Не активируем участника автоматически!
|
||||||
if hasSpeakers {
|
|
||||||
t.emit(EventSpeakerChanged)
|
|
||||||
}
|
|
||||||
|
|
||||||
go t.tick()
|
go t.tick()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -461,7 +453,9 @@ func (t *Timer) buildState() models.TimerState {
|
|||||||
|
|
||||||
if t.running && !t.paused {
|
if t.running && !t.paused {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
speakerElapsed = now.Sub(t.speakerStartTime)
|
if t.currentSpeakerID != 0 {
|
||||||
|
speakerElapsed = now.Sub(t.speakerStartTime)
|
||||||
|
}
|
||||||
meetingElapsed = now.Sub(t.meetingStartTime)
|
meetingElapsed = now.Sub(t.meetingStartTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -519,9 +513,15 @@ func (t *Timer) tick() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
speakerElapsed := now.Sub(t.speakerStartTime)
|
|
||||||
meetingElapsed := now.Sub(t.meetingStartTime)
|
meetingElapsed := now.Sub(t.meetingStartTime)
|
||||||
|
|
||||||
|
if t.currentSpeakerID == 0 {
|
||||||
|
t.mu.Unlock()
|
||||||
|
t.emit(EventTick)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
speakerElapsed := now.Sub(t.speakerStartTime)
|
||||||
remaining := t.speakerLimit - speakerElapsed
|
remaining := t.speakerLimit - speakerElapsed
|
||||||
if !t.speakerWarned && remaining <= t.warningThreshold && remaining > 0 {
|
if !t.speakerWarned && remaining <= t.warningThreshold && remaining > 0 {
|
||||||
t.speakerWarned = true
|
t.speakerWarned = true
|
||||||
|
|||||||
Reference in New Issue
Block a user