Implement alarm sound

This commit is contained in:
nemunaire 2022-10-14 20:08:03 +02:00
parent b7dbc597d8
commit df31c4dcd1
15 changed files with 531 additions and 50 deletions

47
api/alarm.go Normal file
View File

@ -0,0 +1,47 @@
package api
import (
"net/http"
"github.com/gin-gonic/gin"
"git.nemunai.re/nemunaire/reveil/config"
"git.nemunai.re/nemunaire/reveil/player"
)
func declareAlarmRoutes(cfg *config.Config, router *gin.RouterGroup) {
router.GET("/alarm", func(c *gin.Context) {
if player.CommonPlayer == nil {
c.JSON(http.StatusOK, false)
} else {
c.JSON(http.StatusOK, true)
}
})
router.POST("/alarm/run", func(c *gin.Context) {
if player.CommonPlayer == nil {
err := player.WakeUp(cfg)
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
} else {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Player already running"})
return
}
c.JSON(http.StatusOK, true)
})
router.DELETE("/alarm", func(c *gin.Context) {
if player.CommonPlayer != nil {
err := player.CommonPlayer.Stop()
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
}
c.JSON(http.StatusOK, true)
})
}

View File

@ -11,6 +11,7 @@ func DeclareRoutes(router *gin.Engine, cfg *config.Config, db *reveil.LevelDBSto
apiRoutes := router.Group("/api") apiRoutes := router.Group("/api")
declareActionsRoutes(cfg, apiRoutes) declareActionsRoutes(cfg, apiRoutes)
declareAlarmRoutes(cfg, apiRoutes)
declareAlarmsRoutes(cfg, db, resetTimer, apiRoutes) declareAlarmsRoutes(cfg, db, resetTimer, apiRoutes)
declareGongsRoutes(cfg, apiRoutes) declareGongsRoutes(cfg, apiRoutes)
declareHistoryRoutes(cfg, apiRoutes) declareHistoryRoutes(cfg, apiRoutes)

8
app.go
View File

@ -11,6 +11,7 @@ import (
"git.nemunai.re/nemunaire/reveil/api" "git.nemunai.re/nemunaire/reveil/api"
"git.nemunai.re/nemunaire/reveil/config" "git.nemunai.re/nemunaire/reveil/config"
"git.nemunai.re/nemunaire/reveil/model" "git.nemunai.re/nemunaire/reveil/model"
"git.nemunai.re/nemunaire/reveil/player"
"git.nemunai.re/nemunaire/reveil/ui" "git.nemunai.re/nemunaire/reveil/ui"
) )
@ -80,7 +81,12 @@ func (app *App) ResetTimer() {
if na, err := reveil.GetNextAlarm(app.db); err == nil && na != nil { if na, err := reveil.GetNextAlarm(app.db); err == nil && na != nil {
app.nextAlarm = time.AfterFunc(time.Until(*na), func() { app.nextAlarm = time.AfterFunc(time.Until(*na), func() {
app.nextAlarm = nil app.nextAlarm = nil
log.Println("RUN WAKEUP FUNC") reveil.RemoveOldAlarmsSingle(app.db)
err := player.WakeUp(app.cfg)
if err != nil {
log.Println(err.Error())
return
}
}) })
log.Println("Next timer programmed for", *na) log.Println("Next timer programmed for", *na)
} }

View File

@ -16,6 +16,7 @@ func (c *Config) declareFlags() {
flag.StringVar(&c.GongsDir, "gongs-dir", c.GongsDir, "Path to the directory containing the gongs") flag.StringVar(&c.GongsDir, "gongs-dir", c.GongsDir, "Path to the directory containing the gongs")
flag.StringVar(&c.ActionsDir, "actions-dir", c.ActionsDir, "Path to the directory containing the actions") flag.StringVar(&c.ActionsDir, "actions-dir", c.ActionsDir, "Path to the directory containing the actions")
flag.StringVar(&c.RoutinesDir, "routines-dir", c.RoutinesDir, "Path to the directory containing the routines") flag.StringVar(&c.RoutinesDir, "routines-dir", c.RoutinesDir, "Path to the directory containing the routines")
flag.IntVar(&c.SampleRate, "samplerate", c.SampleRate, "Samplerate for unifying output stream")
// Others flags are declared in some other files when they need specials configurations // Others flags are declared in some other files when they need specials configurations
} }
@ -30,6 +31,7 @@ func Consolidated() (cfg *Config, err error) {
GongsDir: "./gongs/", GongsDir: "./gongs/",
ActionsDir: "./actions/", ActionsDir: "./actions/",
RoutinesDir: "./routines/", RoutinesDir: "./routines/",
SampleRate: 44100,
} }
cfg.declareFlags() cfg.declareFlags()

View File

@ -6,16 +6,19 @@ import (
) )
type Config struct { type Config struct {
DevProxy string DevProxy string
Bind string Bind string
ExternalURL URL ExternalURL URL
BaseURL string BaseURL string
LevelDBPath string LevelDBPath string
SettingsFile string SettingsFile string
TracksDir string TracksDir string
GongsDir string GongsDir string
ActionsDir string ActionsDir string
RoutinesDir string RoutinesDir string
SampleRate int
} }
// parseLine treats a config line and place the read value in the variable // parseLine treats a config line and place the read value in the variable

12
go.mod
View File

@ -3,28 +3,38 @@ module git.nemunai.re/nemunaire/reveil
go 1.18 go 1.18
require ( require (
github.com/faiface/beep v0.0.0-00010101000000-000000000000
github.com/gin-gonic/gin v1.8.1 github.com/gin-gonic/gin v1.8.1
github.com/syndtr/goleveldb v1.0.0 github.com/syndtr/goleveldb v1.0.0
) )
require ( require (
github.com/ebitengine/purego v0.0.0-20220907032450-cf3e27c364c7 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.0 // indirect github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/go-playground/validator/v10 v10.10.0 // indirect github.com/go-playground/validator/v10 v10.10.0 // indirect
github.com/goccy/go-json v0.9.7 // indirect github.com/goccy/go-json v0.9.7 // indirect
github.com/golang/snappy v0.0.1 // indirect github.com/golang/snappy v0.0.1 // indirect
github.com/hajimehoshi/go-mp3 v0.3.0 // indirect
github.com/hajimehoshi/oto/v2 v2.4.0-alpha.4 // indirect
github.com/icza/bitio v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/leodido/go-urn v1.2.1 // indirect github.com/leodido/go-urn v1.2.1 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mewkiz/flac v1.0.7 // indirect
github.com/mewkiz/pkg v0.0.0-20190919212034-518ade7978e2 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.0.1 // indirect github.com/pelletier/go-toml/v2 v2.0.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/ugorji/go/codec v1.2.7 // indirect github.com/ugorji/go/codec v1.2.7 // indirect
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e // indirect golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e // indirect
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e // indirect
golang.org/x/text v0.3.7 // indirect golang.org/x/text v0.3.7 // indirect
google.golang.org/protobuf v1.28.0 // indirect google.golang.org/protobuf v1.28.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
) )
replace github.com/faiface/beep => github.com/MarkKremer/beep v1.0.3-0.20221013180303-756ceb286755

43
go.sum
View File

@ -1,12 +1,23 @@
github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
github.com/MarkKremer/beep v1.0.3-0.20221013180303-756ceb286755 h1:rkuKNEd+Izze/hA44R1kzPs9BXa544xXeufys8x0fkc=
github.com/MarkKremer/beep v1.0.3-0.20221013180303-756ceb286755/go.mod h1:PWWzyIlbyHQjQ/gJzGiMyDAvjo/t5L8TC8qkyX8UfWs=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/d4l3k/messagediff v1.2.2-0.20190829033028-7e0a312ae40b/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkEQxENCrlLo=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/ebitengine/purego v0.0.0-20220907032450-cf3e27c364c7 h1:tmSauY5l3s/Cp5n+cEiG1epUR2AejmdHeMJMycMFxb0=
github.com/ebitengine/purego v0.0.0-20220907032450-cf3e27c364c7/go.mod h1:Eh8I3yvknDYZeCuXH9kRNaPuHEwvXDCk378o9xszmHg=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8= github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8=
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
github.com/go-audio/audio v1.0.0/go.mod h1:6uAu0+H2lHkwdGsAY+j2wHPNPpPoeg5AaEFh9FlA+Zs=
github.com/go-audio/riff v1.0.0/go.mod h1:l3cQwc85y79NQFCRB7TiPoNiaijp6q8Z0Uv38rVG498=
github.com/go-audio/wav v1.0.0/go.mod h1:3yoReyQOsiARkvPl3ERCi8JFjihzG6WhjYpZCf5zAWE=
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
@ -25,8 +36,19 @@ github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/hajimehoshi/go-mp3 v0.3.0 h1:fTM5DXjp/DL2G74HHAs/aBGiS9Tg7wnp+jkU38bHy4g=
github.com/hajimehoshi/go-mp3 v0.3.0/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM=
github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI=
github.com/hajimehoshi/oto/v2 v2.4.0-alpha.4 h1:m29xzbn3Pv5MgvgjMPs7m28uhUgVt3B3AIGjQLgkqUI=
github.com/hajimehoshi/oto/v2 v2.4.0-alpha.4/go.mod h1:OdGUICBjy7upAjvqqacbB63XIuYR3fqXZ7kYtlVYJgQ=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/icza/bitio v1.0.0 h1:squ/m1SHyFeCA6+6Gyol1AxV9nmPPlJFT8c2vKdj3U8=
github.com/icza/bitio v1.0.0/go.mod h1:0jGnlLAx8MKMr9VGnn/4YrvZiprkvBelsVIbA9Jjr9A=
github.com/icza/mighty v0.0.0-20180919140131-cfd07d671de6 h1:8UsGZ2rr2ksmEru6lToqnXgA8Mz1DP11X4zSJ159C3k=
github.com/icza/mighty v0.0.0-20180919140131-cfd07d671de6/go.mod h1:xQig96I1VNBDIWGCdTt54nHt6EeI639SmHycLYL7FkA=
github.com/jfreymuth/oggvorbis v1.0.1/go.mod h1:NqS+K+UXKje0FUYUPosyQ+XTVvjmVjps1aEZH1sumIk=
github.com/jfreymuth/vorbis v1.0.0/go.mod h1:8zy3lUAm9K/rJJk223RKy6vjCZTWC61NA2QD06bfOE0=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@ -39,8 +61,14 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mewkiz/flac v1.0.7 h1:uIXEjnuXqdRaZttmSFM5v5Ukp4U6orrZsnYGGR3yow8=
github.com/mewkiz/flac v1.0.7/go.mod h1:yU74UH277dBUpqxPouHSQIar3G1X/QIclVbFahSd1pU=
github.com/mewkiz/pkg v0.0.0-20190919212034-518ade7978e2 h1:EyTNMdePWaoWsRSGQnXiSoQu0r6RS1eA557AwJhlzHU=
github.com/mewkiz/pkg v0.0.0-20190919212034-518ade7978e2/go.mod h1:3E2FUC/qYUfM8+r9zAwpeHJzqRVVMIYnpzD/clwWxyA=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
@ -53,6 +81,9 @@ github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1Cpa
github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU= github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU=
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
@ -72,18 +103,26 @@ github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/image v0.0.0-20190220214146-31aff87c08e9/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e h1:TsQ7F31D3bUCLeqPT0u+yjp1guoArKaNKmCr22PYgTQ= golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e h1:TsQ7F31D3bUCLeqPT0u+yjp1guoArKaNKmCr22PYgTQ=
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k= golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e h1:NHvCuwuS43lGnYhten69ZWqi2QOj/CiDNcKbVqwVoew=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=

View File

@ -250,6 +250,25 @@ func DeleteAlarmSingle(db *LevelDBStorage, alarm *AlarmSingle) (err error) {
return db.delete(fmt.Sprintf("alarm-single-%s", alarm.Id.ToString())) return db.delete(fmt.Sprintf("alarm-single-%s", alarm.Id.ToString()))
} }
func RemoveOldAlarmsSingle(db *LevelDBStorage) error {
alarms, err := GetAlarmsSingle(db)
if err != nil {
return err
}
now := time.Now()
for _, alarm := range alarms {
if now.After(time.Time(alarm.Time)) {
err = DeleteAlarmSingle(db, alarm)
if err != nil {
return err
}
}
}
return nil
}
type AlarmException struct { type AlarmException struct {
Id Identifier `json:"id"` Id Identifier `json:"id"`
Start *Date `json:"start"` Start *Date `json:"start"`

View File

@ -19,13 +19,13 @@ type Gong struct {
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
} }
func currentGongPath(cfg *config.Config) string { func CurrentGongPath(cfg *config.Config) string {
return filepath.Join(cfg.GongsDir, CURRENT_GONG) return filepath.Join(cfg.GongsDir, CURRENT_GONG)
} }
func LoadGongs(cfg *config.Config) (gongs []*Gong, err error) { func LoadGongs(cfg *config.Config) (gongs []*Gong, err error) {
// Retrieve the path of the current gong // Retrieve the path of the current gong
current_gong, err := os.Readlink(currentGongPath(cfg)) current_gong, err := os.Readlink(CurrentGongPath(cfg))
if err == nil { if err == nil {
current_gong, _ = filepath.Abs(filepath.Join(cfg.GongsDir, current_gong)) current_gong, _ = filepath.Abs(filepath.Join(cfg.GongsDir, current_gong))
} }
@ -65,7 +65,7 @@ func (g *Gong) Rename(newName string) error {
} }
func (g *Gong) SetDefault(cfg *config.Config) error { func (g *Gong) SetDefault(cfg *config.Config) error {
linkpath := currentGongPath(cfg) linkpath := CurrentGongPath(cfg)
os.Remove(linkpath) os.Remove(linkpath)
pabs, err := filepath.Abs(g.Path) pabs, err := filepath.Abs(g.Path)

View File

@ -12,6 +12,7 @@ type Settings struct {
GongInterval time.Duration `json:"gong_interval"` GongInterval time.Duration `json:"gong_interval"`
WeatherDelay time.Duration `json:"weather_delay"` WeatherDelay time.Duration `json:"weather_delay"`
WeatherAction string `json:"weather_action"` WeatherAction string `json:"weather_action"`
MaxRunTime time.Duration `json:"max_run_time"`
} }
// ExistsSettings checks if the settings file can by found at the given path. // ExistsSettings checks if the settings file can by found at the given path.

256
player/player.go Normal file
View File

@ -0,0 +1,256 @@
package player
import (
"fmt"
"log"
"math"
"math/rand"
"os"
"os/signal"
"path"
"strings"
"syscall"
"time"
"github.com/faiface/beep"
"github.com/faiface/beep/effects"
"github.com/faiface/beep/flac"
"github.com/faiface/beep/mp3"
"github.com/faiface/beep/speaker"
"github.com/faiface/beep/wav"
"git.nemunai.re/nemunaire/reveil/config"
"git.nemunai.re/nemunaire/reveil/model"
)
var CommonPlayer *Player
type Player struct {
Playlist []string
MaxRunTime time.Duration
Stopper chan bool
sampleRate beep.SampleRate
claironTime time.Duration
claironFile string
ntick int64
hasClaironed bool
launched time.Time
volume *effects.Volume
dontUpdateVolume bool
reverseOrder bool
playedItem int
}
func WakeUp(cfg *config.Config) (err error) {
if CommonPlayer != nil {
return fmt.Errorf("Unable to start the player: a player is already running")
}
CommonPlayer, err = NewPlayer(cfg)
if err != nil {
return err
}
go CommonPlayer.WakeUp()
return nil
}
func NewPlayer(cfg *config.Config) (*Player, error) {
// Load our settings
settings, err := reveil.ReadSettings(cfg.SettingsFile)
if err != nil {
return nil, fmt.Errorf("Unable to read settings: %w", err)
}
p := Player{
Stopper: make(chan bool, 1),
MaxRunTime: settings.MaxRunTime * time.Minute,
sampleRate: beep.SampleRate(cfg.SampleRate),
claironTime: settings.GongInterval * time.Minute,
claironFile: reveil.CurrentGongPath(cfg),
}
// Load our track list
tracks, err := reveil.LoadTracks(cfg)
if err != nil {
return nil, fmt.Errorf("Unable to load tracks: %w", err)
}
var playlist []string
// Creating playlist
log.Println("Loading playlist...")
for _, track := range tracks {
if !track.Enabled {
continue
}
p.Playlist = append(p.Playlist, track.Path)
}
log.Println("Shuffling playlist...")
// Shuffle the playlist
rand.Shuffle(len(playlist), func(i, j int) {
playlist[i], playlist[j] = playlist[j], playlist[i]
})
return &p, nil
}
func loadFile(filepath string) (name string, s beep.StreamSeekCloser, format beep.Format, err error) {
var fd *os.File
name = path.Base(filepath)
fd, err = os.Open(filepath)
if err != nil {
return
}
switch strings.ToLower(path.Ext(filepath)) {
case ".flac":
s, format, err = flac.Decode(fd)
case ".mp3":
s, format, err = mp3.Decode(fd)
default:
s, format, err = wav.Decode(fd)
}
if err != nil {
fd.Close()
return
}
return
}
func (p *Player) WakeUp() {
log.Println("RUN WAKEUP FUNC")
log.Println("Playlist in use:", strings.Join(p.Playlist, " ; "))
// Create infinite stream
stream := beep.Iterate(func() beep.Streamer {
if !p.hasClaironed && time.Since(p.launched) >= p.claironTime {
log.Println("clairon time!")
p.claironTime += p.claironTime / 2
_, sample, format, err := loadFile(p.claironFile)
if err == nil {
p.volume.Volume = 0.1
p.dontUpdateVolume = true
if format.SampleRate != p.sampleRate {
return beep.Resample(3, format.SampleRate, p.sampleRate, sample)
} else {
return sample
}
} else {
log.Println("Error loading clairon:", err)
}
}
p.dontUpdateVolume = false
p.volume.Volume = -2 - math.Log(5/float64(p.ntick))/3
if p.reverseOrder {
p.playedItem -= 1
} else {
p.playedItem += 1
}
if p.playedItem >= len(p.Playlist) {
p.playedItem = 0
} else if p.playedItem < 0 {
p.playedItem = len(p.Playlist) - 1
}
// Load our current item
_, sample, format, err := loadFile(p.Playlist[p.playedItem])
if err != nil {
log.Println("Error loading audio file %s: %s", p.Playlist[p.playedItem], err.Error())
return nil
}
// Resample if needed
log.Println("playing list item:", p.playedItem, "/", len(p.Playlist), ":", p.Playlist[p.playedItem])
if format.SampleRate != p.sampleRate {
return beep.Resample(3, format.SampleRate, p.sampleRate, sample)
} else {
return sample
}
})
// Prepare sound player
log.Println("Initializing sound player...")
speaker.Init(p.sampleRate, p.sampleRate.N(time.Second/10))
defer speaker.Close()
p.volume = &effects.Volume{stream, 10, -2, false}
speaker.Play(p.volume)
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
p.launched = time.Now()
// Prepare graceful shutdown
maxRun := time.After(p.MaxRunTime)
interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, os.Interrupt, syscall.SIGHUP)
loop:
for {
select {
case <-p.Stopper:
log.Println("Stopper activated")
break loop
case <-maxRun:
log.Println("Max run time exhausted")
break loop
case <-ticker.C:
p.ntick += 1
if !p.dontUpdateVolume {
p.volume.Volume = -2 - math.Log(5/float64(p.ntick))/3
}
case <-interrupt:
break loop
}
}
log.Println("Stopping the player...")
// Calm down music
loopcalm:
for i := 0; i < 2000; i += 1 {
p.volume.Volume -= 0.001
timer := time.NewTimer(4 * time.Millisecond)
select {
case <-p.Stopper:
log.Println("Hard stop received...")
timer.Stop()
p.volume.Volume = 0
break loopcalm
case <-timer.C:
break
}
}
if p == CommonPlayer {
log.Println("Destoying common player")
CommonPlayer = nil
// TODO: find a better way to deallocate the card
os.Exit(42)
}
}
func (p *Player) Stop() error {
log.Println("Trying to stop the player")
p.Stopper <- true
return nil
}

46
ui/src/lib/alarm.js Normal file
View File

@ -0,0 +1,46 @@
export async function isAlarmActive() {
const res = await fetch('api/alarm', {
headers: {'Accept': 'application/json'},
});
if (res.status == 200) {
return await res.json();
} else {
throw new Error((await res.json()).errmsg);
}
}
export async function runAlarm() {
const res = await fetch('api/alarm/run', {
method: 'POST',
headers: {'Accept': 'application/json'},
});
if (res.status == 200) {
return await res.json();
} else {
throw new Error((await res.json()).errmsg);
}
}
export async function alarmNextTrack() {
const res = await fetch('api/alarm/next', {
method: 'POST',
headers: {'Accept': 'application/json'},
});
if (res.status == 200) {
return await res.json();
} else {
throw new Error((await res.json()).errmsg);
}
}
export async function alarmStop() {
const res = await fetch('api/alarm', {
method: 'DELETE',
headers: {'Accept': 'application/json'},
});
if (res.status == 200) {
return await res.json();
} else {
throw new Error((await res.json()).errmsg);
}
}

View File

@ -5,11 +5,12 @@ export class Settings {
} }
} }
update({ language, gong_interval, weather_delay, weather_action }) { update({ language, gong_interval, weather_delay, weather_action, max_run_time }) {
this.language = language; this.language = language;
this.gong_interval = gong_interval; this.gong_interval = gong_interval;
this.weather_delay = weather_delay; this.weather_delay = weather_delay;
this.weather_action = weather_action; this.weather_action = weather_action;
this.max_run_time = max_run_time;
} }
async save() { async save() {

View File

@ -6,20 +6,35 @@
import CycleCounter from '../components/CycleCounter.svelte'; import CycleCounter from '../components/CycleCounter.svelte';
import DateFormat from '../components/DateFormat.svelte'; import DateFormat from '../components/DateFormat.svelte';
import { getNextAlarm, newNCyclesAlarm } from '../lib/alarmsingle'; import { isAlarmActive, alarmNextTrack, runAlarm, alarmStop } from '$lib/alarm';
import { getNextAlarm, newNCyclesAlarm } from '$lib/alarmsingle';
import { alarmsSingle } from '../stores/alarmsingle'; import { alarmsSingle } from '../stores/alarmsingle';
import { quotes } from '../stores/quotes'; import { quotes } from '../stores/quotes';
let nextAlarmP = getNextAlarm(); let nextAlarmP = getNextAlarm();
let isActiveP = isAlarmActive();
function reloadNextAlarm() { function reloadNextAlarm() {
nextAlarmP = getNextAlarm(); nextAlarmP = getNextAlarm();
alarmsSingle.clear(); alarmsSingle.clear();
} }
function reloadIsActiveAlarm() {
isActiveP = isAlarmActive();
return isActiveP;
}
function newCyclesAlarm(ncycles) { function newCyclesAlarm(ncycles) {
newNCyclesAlarm(ncycles).then(reloadNextAlarm); newNCyclesAlarm(ncycles).then(reloadNextAlarm);
} }
function stopAlarm() {
alarmStop();
reloadIsActiveAlarm().then((isActive) => {
if (isActive) {
setTimeout(reloadIsActiveAlarm, 10000);
}
})
}
</script> </script>
<Container class="flex-fill d-flex flex-column justify-content-center text-center"> <Container class="flex-fill d-flex flex-column justify-content-center text-center">
@ -41,6 +56,9 @@
{/if} {/if}
<div class="display-5 mb-5"> <div class="display-5 mb-5">
{#await nextAlarmP} {#await nextAlarmP}
<div class="spinner-border" role="status">
<span class="visually-hidden">Loading...</span>
</div>
{:then nextalarm} {:then nextalarm}
Prochain réveil&nbsp;: Prochain réveil&nbsp;:
{#if nextalarm.getDay() == new Date().getDay() && nextalarm.getMonth() == new Date().getMonth() && nextalarm.getFullYear() == new Date().getFullYear()} {#if nextalarm.getDay() == new Date().getDay() && nextalarm.getMonth() == new Date().getMonth() && nextalarm.getFullYear() == new Date().getFullYear()}
@ -62,41 +80,59 @@
{/if} {/if}
{/await} {/await}
</div> </div>
<div class="d-flex gap-3 justify-content-center"> {#await isActiveP then isActive}
<a {#if !isActive}
href="alarms/single/new" <div class="d-flex gap-3 justify-content-center">
class="btn btn-primary" <a
> href="alarms/single/new"
<Icon name="node-plus" /> class="btn btn-primary"
Programmer un nouveau réveil >
</a> <Icon name="node-plus" />
<button Programmer un nouveau réveil
class="btn btn-info" </a>
on:click={() => newCyclesAlarm(5)} <button
> class="btn btn-info"
<Icon name="node-plus" /> on:click={() => newCyclesAlarm(5)}
5 cycles >
</button> <Icon name="node-plus" />
<button 5 cycles
class="btn btn-info" </button>
on:click={() => newCyclesAlarm(6)} <button
> class="btn btn-info"
<Icon name="node-plus" /> on:click={() => newCyclesAlarm(6)}
6 cycles >
</button> <Icon name="node-plus" />
</div> 6 cycles
<div class="d-flex gap-3 mt-3 justify-content-center"> </button>
<button class="btn btn-outline-info"> <button
<Icon name="skip-end-fill" /> class="btn btn-outline-warning"
Chanson suivante on:click={() => { runAlarm(); reloadIsActiveAlarm(); }}
</button> >
<button class="btn btn-danger"> <Icon name="play-circle" />
<Icon name="stop-circle" /> Lancer le réveil
Éteindre le réveil </button>
</button> </div>
<button class="btn btn-outline-info"> {:else}
<Icon name="fast-forward-fill" /> <div class="d-flex gap-3 mt-3 justify-content-center">
Passer cette étape de la routine <button
</button> class="btn btn-outline-info"
</div> on:click={alarmNextTrack}
>
<Icon name="skip-end-fill" />
Chanson suivante
</button>
<button
class="btn btn-danger"
on:click={stopAlarm}
>
<Icon name="stop-circle" />
Éteindre le réveil
</button>
<button class="btn btn-outline-info">
<Icon name="fast-forward-fill" />
Passer cette étape de la routine
</button>
</div>
{/if}
{/await}
</Container> </Container>

View File

@ -95,6 +95,20 @@
<option value="de_DE">Allemand</option> <option value="de_DE">Allemand</option>
</Input> </Input>
</FormGroup> </FormGroup>
<FormGroup>
<Label for="maxRunTime">Durée maximale d'exécution du réveil</Label>
<InputGroup>
<Input
type="number"
id="maxRunTime"
placeholder="60"
bind:value={settings.max_run_time}
on:change={() => settings.save()}
/>
<InputGroupText>min</InputGroupText>
</InputGroup>
</FormGroup>
{/await} {/await}
</Form> </Form>
</Container> </Container>