[add] settings (pw & time offset), [fix] PWA issues, [fix] misc styling issues
This commit is contained in:
parent
25ab36e4b5
commit
2757f10bfc
@ -14,6 +14,7 @@ import (
|
|||||||
"reichard.io/bbank/config"
|
"reichard.io/bbank/config"
|
||||||
"reichard.io/bbank/database"
|
"reichard.io/bbank/database"
|
||||||
"reichard.io/bbank/graph"
|
"reichard.io/bbank/graph"
|
||||||
|
"reichard.io/bbank/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type API struct {
|
type API struct {
|
||||||
@ -74,11 +75,13 @@ func (api *API) registerWebAppRoutes() {
|
|||||||
render := multitemplate.NewRenderer()
|
render := multitemplate.NewRenderer()
|
||||||
helperFuncs := template.FuncMap{
|
helperFuncs := template.FuncMap{
|
||||||
"GetSVGGraphData": graph.GetSVGGraphData,
|
"GetSVGGraphData": graph.GetSVGGraphData,
|
||||||
|
"GetUTCOffsets": utils.GetUTCOffsets,
|
||||||
}
|
}
|
||||||
|
|
||||||
render.AddFromFilesFuncs("login", helperFuncs, "templates/login.html")
|
render.AddFromFilesFuncs("login", helperFuncs, "templates/login.html")
|
||||||
render.AddFromFilesFuncs("home", helperFuncs, "templates/base.html", "templates/home.html")
|
render.AddFromFilesFuncs("home", helperFuncs, "templates/base.html", "templates/home.html")
|
||||||
render.AddFromFilesFuncs("graphs", helperFuncs, "templates/base.html", "templates/graphs.html")
|
render.AddFromFilesFuncs("graphs", helperFuncs, "templates/base.html", "templates/graphs.html")
|
||||||
|
render.AddFromFilesFuncs("settings", helperFuncs, "templates/base.html", "templates/settings.html")
|
||||||
render.AddFromFilesFuncs("activity", helperFuncs, "templates/base.html", "templates/activity.html")
|
render.AddFromFilesFuncs("activity", helperFuncs, "templates/base.html", "templates/activity.html")
|
||||||
render.AddFromFilesFuncs("documents", helperFuncs, "templates/base.html", "templates/documents.html")
|
render.AddFromFilesFuncs("documents", helperFuncs, "templates/base.html", "templates/documents.html")
|
||||||
render.AddFromFilesFuncs("document", helperFuncs, "templates/base.html", "templates/document.html")
|
render.AddFromFilesFuncs("document", helperFuncs, "templates/base.html", "templates/document.html")
|
||||||
@ -93,6 +96,8 @@ func (api *API) registerWebAppRoutes() {
|
|||||||
api.Router.POST("/register", api.authFormRegister)
|
api.Router.POST("/register", api.authFormRegister)
|
||||||
|
|
||||||
api.Router.GET("/", api.authWebAppMiddleware, api.createAppResourcesRoute("home"))
|
api.Router.GET("/", api.authWebAppMiddleware, api.createAppResourcesRoute("home"))
|
||||||
|
api.Router.GET("/settings", api.authWebAppMiddleware, api.createAppResourcesRoute("settings"))
|
||||||
|
api.Router.POST("/settings", api.authWebAppMiddleware, api.editSettings)
|
||||||
api.Router.GET("/activity", api.authWebAppMiddleware, api.createAppResourcesRoute("activity"))
|
api.Router.GET("/activity", api.authWebAppMiddleware, api.createAppResourcesRoute("activity"))
|
||||||
api.Router.GET("/documents", api.authWebAppMiddleware, api.createAppResourcesRoute("documents"))
|
api.Router.GET("/documents", api.authWebAppMiddleware, api.createAppResourcesRoute("documents"))
|
||||||
api.Router.GET("/documents/:document", api.authWebAppMiddleware, api.createAppResourcesRoute("document"))
|
api.Router.GET("/documents/:document", api.authWebAppMiddleware, api.createAppResourcesRoute("document"))
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/md5"
|
||||||
"fmt"
|
"fmt"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -9,6 +10,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
argon2 "github.com/alexedwards/argon2id"
|
||||||
"github.com/gabriel-vasile/mimetype"
|
"github.com/gabriel-vasile/mimetype"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
@ -40,6 +42,12 @@ type requestDocumentIdentify struct {
|
|||||||
ISBN *string `form:"isbn"`
|
ISBN *string `form:"isbn"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type requestSettingsEdit struct {
|
||||||
|
Password *string `form:"password"`
|
||||||
|
NewPassword *string `form:"new_password"`
|
||||||
|
TimeOffset *string `form:"time_offset"`
|
||||||
|
}
|
||||||
|
|
||||||
func baseResourceRoute(template string, args ...map[string]any) func(c *gin.Context) {
|
func baseResourceRoute(template string, args ...map[string]any) func(c *gin.Context) {
|
||||||
variables := gin.H{"RouteName": template}
|
variables := gin.H{"RouteName": template}
|
||||||
if len(args) > 0 {
|
if len(args) > 0 {
|
||||||
@ -167,6 +175,27 @@ func (api *API) createAppResourcesRoute(routeName string, args ...map[string]any
|
|||||||
"DatabaseInfo": database_info,
|
"DatabaseInfo": database_info,
|
||||||
"GraphData": read_graph_data,
|
"GraphData": read_graph_data,
|
||||||
}
|
}
|
||||||
|
} else if routeName == "settings" {
|
||||||
|
user, err := api.DB.Queries.GetUser(api.DB.Ctx, rUser.(string))
|
||||||
|
if err != nil {
|
||||||
|
log.Error("[createAppResourcesRoute] GetUser DB Error:", err)
|
||||||
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
devices, err := api.DB.Queries.GetDevices(api.DB.Ctx, rUser.(string))
|
||||||
|
if err != nil {
|
||||||
|
log.Error("[createAppResourcesRoute] GetDevices DB Error:", err)
|
||||||
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
templateVars["Data"] = gin.H{
|
||||||
|
"Settings": gin.H{
|
||||||
|
"TimeOffset": *user.TimeOffset,
|
||||||
|
},
|
||||||
|
"Devices": devices,
|
||||||
|
}
|
||||||
} else if routeName == "login" {
|
} else if routeName == "login" {
|
||||||
templateVars["RegistrationEnabled"] = api.Config.RegistrationEnabled
|
templateVars["RegistrationEnabled"] = api.Config.RegistrationEnabled
|
||||||
}
|
}
|
||||||
@ -471,6 +500,88 @@ func (api *API) identifyDocument(c *gin.Context) {
|
|||||||
c.HTML(http.StatusOK, "document", templateVars)
|
c.HTML(http.StatusOK, "document", templateVars)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (api *API) editSettings(c *gin.Context) {
|
||||||
|
rUser, _ := c.Get("AuthorizedUser")
|
||||||
|
|
||||||
|
var rUserSettings requestSettingsEdit
|
||||||
|
if err := c.ShouldBind(&rUserSettings); err != nil {
|
||||||
|
log.Error("[editSettings] Invalid Form Bind")
|
||||||
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate Something Exists
|
||||||
|
if rUserSettings.Password == nil && rUserSettings.NewPassword == nil && rUserSettings.TimeOffset == nil {
|
||||||
|
log.Error("[editSettings] Missing Form Values")
|
||||||
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
templateVars := gin.H{
|
||||||
|
"User": rUser,
|
||||||
|
}
|
||||||
|
newUserSettings := database.UpdateUserParams{
|
||||||
|
UserID: rUser.(string),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set New Password
|
||||||
|
if rUserSettings.Password != nil && rUserSettings.NewPassword != nil {
|
||||||
|
password := fmt.Sprintf("%x", md5.Sum([]byte(*rUserSettings.Password)))
|
||||||
|
authorized := api.authorizeCredentials(rUser.(string), password)
|
||||||
|
if authorized == true {
|
||||||
|
password := fmt.Sprintf("%x", md5.Sum([]byte(*rUserSettings.NewPassword)))
|
||||||
|
hashedPassword, err := argon2.CreateHash(password, argon2.DefaultParams)
|
||||||
|
if err != nil {
|
||||||
|
templateVars["PasswordErrorMessage"] = "Unknown Error"
|
||||||
|
} else {
|
||||||
|
templateVars["PasswordMessage"] = "Password Updated"
|
||||||
|
newUserSettings.Password = &hashedPassword
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templateVars["PasswordErrorMessage"] = "Invalid Password"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set Time Offset
|
||||||
|
if rUserSettings.TimeOffset != nil {
|
||||||
|
templateVars["TimeOffsetMessage"] = "Time Offset Updated"
|
||||||
|
newUserSettings.TimeOffset = rUserSettings.TimeOffset
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update User
|
||||||
|
_, err := api.DB.Queries.UpdateUser(api.DB.Ctx, newUserSettings)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("[editSettings] UpdateUser DB Error:", err)
|
||||||
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get User
|
||||||
|
user, err := api.DB.Queries.GetUser(api.DB.Ctx, rUser.(string))
|
||||||
|
if err != nil {
|
||||||
|
log.Error("[editSettings] GetUser DB Error:", err)
|
||||||
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get Devices
|
||||||
|
devices, err := api.DB.Queries.GetDevices(api.DB.Ctx, rUser.(string))
|
||||||
|
if err != nil {
|
||||||
|
log.Error("[editSettings] GetDevices DB Error:", err)
|
||||||
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
templateVars["Data"] = gin.H{
|
||||||
|
"Settings": gin.H{
|
||||||
|
"TimeOffset": *user.TimeOffset,
|
||||||
|
},
|
||||||
|
"Devices": devices,
|
||||||
|
}
|
||||||
|
|
||||||
|
c.HTML(http.StatusOK, "settings", templateVars)
|
||||||
|
}
|
||||||
|
|
||||||
func bindQueryParams(c *gin.Context) queryParams {
|
func bindQueryParams(c *gin.Context) queryParams {
|
||||||
var qParams queryParams
|
var qParams queryParams
|
||||||
c.BindQuery(&qParams)
|
c.BindQuery(&qParams)
|
||||||
|
@ -24,7 +24,7 @@ func (api *API) authorizeCredentials(username string, password string) (authoriz
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if match, err := argon2.ComparePasswordAndHash(password, user.Pass); err != nil || match != true {
|
if match, err := argon2.ComparePasswordAndHash(password, *user.Pass); err != nil || match != true {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,7 +94,6 @@ func (api *API) authFormLogin(c *gin.Context) {
|
|||||||
|
|
||||||
// MD5 - KOSync Compatiblity
|
// MD5 - KOSync Compatiblity
|
||||||
password := fmt.Sprintf("%x", md5.Sum([]byte(rawPassword)))
|
password := fmt.Sprintf("%x", md5.Sum([]byte(rawPassword)))
|
||||||
|
|
||||||
if authorized := api.authorizeCredentials(username, password); authorized != true {
|
if authorized := api.authorizeCredentials(username, password); authorized != true {
|
||||||
c.HTML(http.StatusUnauthorized, "login", gin.H{
|
c.HTML(http.StatusUnauthorized, "login", gin.H{
|
||||||
"RegistrationEnabled": api.Config.RegistrationEnabled,
|
"RegistrationEnabled": api.Config.RegistrationEnabled,
|
||||||
@ -140,7 +139,7 @@ func (api *API) authFormRegister(c *gin.Context) {
|
|||||||
|
|
||||||
rows, err := api.DB.Queries.CreateUser(api.DB.Ctx, database.CreateUserParams{
|
rows, err := api.DB.Queries.CreateUser(api.DB.Ctx, database.CreateUserParams{
|
||||||
ID: username,
|
ID: username,
|
||||||
Pass: hashedPassword,
|
Pass: &hashedPassword,
|
||||||
})
|
})
|
||||||
|
|
||||||
// SQL Error
|
// SQL Error
|
||||||
|
@ -107,7 +107,7 @@ func (api *API) createUser(c *gin.Context) {
|
|||||||
|
|
||||||
rows, err := api.DB.Queries.CreateUser(api.DB.Ctx, database.CreateUserParams{
|
rows, err := api.DB.Queries.CreateUser(api.DB.Ctx, database.CreateUserParams{
|
||||||
ID: rUser.Username,
|
ID: rUser.Username,
|
||||||
Pass: hashedPassword,
|
Pass: &hashedPassword,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("[createUser] CreateUser DB Error:", err)
|
log.Error("[createUser] CreateUser DB Error:", err)
|
||||||
|
@ -2,6 +2,5 @@
|
|||||||
"short_name": "Book Manager",
|
"short_name": "Book Manager",
|
||||||
"name": "Book Manager",
|
"name": "Book Manager",
|
||||||
"theme_color": "#1F2937",
|
"theme_color": "#1F2937",
|
||||||
"background_color": "#1F2937",
|
|
||||||
"display": "standalone"
|
"display": "standalone"
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@ type Config struct {
|
|||||||
|
|
||||||
func Load() *Config {
|
func Load() *Config {
|
||||||
return &Config{
|
return &Config{
|
||||||
Version: "0.0.1",
|
Version: "0.0.2",
|
||||||
DBType: trimLowerString(getEnv("DATABASE_TYPE", "SQLite")),
|
DBType: trimLowerString(getEnv("DATABASE_TYPE", "SQLite")),
|
||||||
DBName: trimLowerString(getEnv("DATABASE_NAME", "book_manager")),
|
DBName: trimLowerString(getEnv("DATABASE_NAME", "book_manager")),
|
||||||
DBPassword: getEnv("DATABASE_PASSWORD", ""),
|
DBPassword: getEnv("DATABASE_PASSWORD", ""),
|
||||||
|
@ -24,7 +24,7 @@ type Device struct {
|
|||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
UserID string `json:"user_id"`
|
UserID string `json:"user_id"`
|
||||||
DeviceName string `json:"device_name"`
|
DeviceName string `json:"device_name"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt string `json:"created_at"`
|
||||||
Sync bool `json:"sync"`
|
Sync bool `json:"sync"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,8 +90,8 @@ type RescaledActivity struct {
|
|||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Pass string `json:"-"`
|
Pass *string `json:"-"`
|
||||||
Admin bool `json:"-"`
|
Admin bool `json:"-"`
|
||||||
TimeOffset string `json:"time_offset"`
|
TimeOffset *string `json:"time_offset"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,14 @@ ON CONFLICT DO NOTHING;
|
|||||||
SELECT * FROM users
|
SELECT * FROM users
|
||||||
WHERE id = $user_id LIMIT 1;
|
WHERE id = $user_id LIMIT 1;
|
||||||
|
|
||||||
|
-- name: UpdateUser :one
|
||||||
|
UPDATE users
|
||||||
|
SET
|
||||||
|
pass = COALESCE($password, pass),
|
||||||
|
time_offset = COALESCE($time_offset, time_offset)
|
||||||
|
WHERE id = $user_id
|
||||||
|
RETURNING *;
|
||||||
|
|
||||||
-- name: UpsertDocument :one
|
-- name: UpsertDocument :one
|
||||||
INSERT INTO documents (
|
INSERT INTO documents (
|
||||||
id,
|
id,
|
||||||
@ -287,11 +295,15 @@ LIMIT $limit
|
|||||||
OFFSET $offset;
|
OFFSET $offset;
|
||||||
|
|
||||||
-- name: GetDevices :many
|
-- name: GetDevices :many
|
||||||
SELECT * FROM devices
|
SELECT
|
||||||
WHERE user_id = $user_id
|
devices.device_name,
|
||||||
ORDER BY created_at DESC
|
CAST(DATETIME(devices.created_at, users.time_offset) AS TEXT) AS created_at,
|
||||||
LIMIT $limit
|
CAST(DATETIME(MAX(activity.created_at), users.time_offset) AS TEXT) AS last_sync
|
||||||
OFFSET $offset;
|
FROM activity
|
||||||
|
JOIN devices ON devices.id = activity.device_id
|
||||||
|
JOIN users ON users.id = $user_id
|
||||||
|
WHERE devices.user_id = $user_id
|
||||||
|
GROUP BY activity.device_id;
|
||||||
|
|
||||||
-- name: GetDocumentReadStats :one
|
-- name: GetDocumentReadStats :one
|
||||||
SELECT
|
SELECT
|
||||||
|
@ -122,7 +122,7 @@ ON CONFLICT DO NOTHING
|
|||||||
|
|
||||||
type CreateUserParams struct {
|
type CreateUserParams struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Pass string `json:"-"`
|
Pass *string `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (int64, error) {
|
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (int64, error) {
|
||||||
@ -376,35 +376,33 @@ func (q *Queries) GetDevice(ctx context.Context, deviceID string) (Device, error
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getDevices = `-- name: GetDevices :many
|
const getDevices = `-- name: GetDevices :many
|
||||||
SELECT id, user_id, device_name, created_at, sync FROM devices
|
SELECT
|
||||||
WHERE user_id = ?1
|
devices.device_name,
|
||||||
ORDER BY created_at DESC
|
CAST(DATETIME(devices.created_at, users.time_offset) AS TEXT) AS created_at,
|
||||||
LIMIT ?3
|
CAST(DATETIME(MAX(activity.created_at), users.time_offset) AS TEXT) AS last_sync
|
||||||
OFFSET ?2
|
FROM activity
|
||||||
|
JOIN devices ON devices.id = activity.device_id
|
||||||
|
JOIN users ON users.id = ?1
|
||||||
|
WHERE devices.user_id = ?1
|
||||||
|
GROUP BY activity.device_id
|
||||||
`
|
`
|
||||||
|
|
||||||
type GetDevicesParams struct {
|
type GetDevicesRow struct {
|
||||||
UserID string `json:"user_id"`
|
DeviceName string `json:"device_name"`
|
||||||
Offset int64 `json:"offset"`
|
CreatedAt string `json:"created_at"`
|
||||||
Limit int64 `json:"limit"`
|
LastSync string `json:"last_sync"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) GetDevices(ctx context.Context, arg GetDevicesParams) ([]Device, error) {
|
func (q *Queries) GetDevices(ctx context.Context, userID string) ([]GetDevicesRow, error) {
|
||||||
rows, err := q.db.QueryContext(ctx, getDevices, arg.UserID, arg.Offset, arg.Limit)
|
rows, err := q.db.QueryContext(ctx, getDevices, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
var items []Device
|
var items []GetDevicesRow
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var i Device
|
var i GetDevicesRow
|
||||||
if err := rows.Scan(
|
if err := rows.Scan(&i.DeviceName, &i.CreatedAt, &i.LastSync); err != nil {
|
||||||
&i.ID,
|
|
||||||
&i.UserID,
|
|
||||||
&i.DeviceName,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.Sync,
|
|
||||||
); err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
items = append(items, i)
|
items = append(items, i)
|
||||||
@ -1272,6 +1270,34 @@ func (q *Queries) UpdateProgress(ctx context.Context, arg UpdateProgressParams)
|
|||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updateUser = `-- name: UpdateUser :one
|
||||||
|
UPDATE users
|
||||||
|
SET
|
||||||
|
pass = COALESCE(?1, pass),
|
||||||
|
time_offset = COALESCE(?2, time_offset)
|
||||||
|
WHERE id = ?3
|
||||||
|
RETURNING id, pass, admin, time_offset, created_at
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpdateUserParams struct {
|
||||||
|
Password *string `json:"-"`
|
||||||
|
TimeOffset *string `json:"time_offset"`
|
||||||
|
UserID string `json:"user_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) (User, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, updateUser, arg.Password, arg.TimeOffset, arg.UserID)
|
||||||
|
var i User
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.Pass,
|
||||||
|
&i.Admin,
|
||||||
|
&i.TimeOffset,
|
||||||
|
&i.CreatedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
const upsertDevice = `-- name: UpsertDevice :one
|
const upsertDevice = `-- name: UpsertDevice :one
|
||||||
INSERT INTO devices (id, user_id, device_name)
|
INSERT INTO devices (id, user_id, device_name)
|
||||||
VALUES (?, ?, ?)
|
VALUES (?, ?, ?)
|
||||||
|
30
sqlc.yaml
30
sqlc.yaml
@ -9,7 +9,7 @@ sql:
|
|||||||
out: "database"
|
out: "database"
|
||||||
emit_json_tags: true
|
emit_json_tags: true
|
||||||
overrides:
|
overrides:
|
||||||
# Type pointers needed for JSON
|
# Documents
|
||||||
- column: "documents.md5"
|
- column: "documents.md5"
|
||||||
go_type:
|
go_type:
|
||||||
type: "string"
|
type: "string"
|
||||||
@ -63,6 +63,7 @@ sql:
|
|||||||
type: "string"
|
type: "string"
|
||||||
pointer: true
|
pointer: true
|
||||||
|
|
||||||
|
# Metadata
|
||||||
- column: "metadata.title"
|
- column: "metadata.title"
|
||||||
go_type:
|
go_type:
|
||||||
type: "string"
|
type: "string"
|
||||||
@ -92,6 +93,33 @@ sql:
|
|||||||
type: "string"
|
type: "string"
|
||||||
pointer: true
|
pointer: true
|
||||||
|
|
||||||
|
# Devices
|
||||||
|
- column: "devices.id"
|
||||||
|
go_type:
|
||||||
|
type: "string"
|
||||||
|
- column: "devices.user_id"
|
||||||
|
go_type:
|
||||||
|
type: "string"
|
||||||
|
- column: "devices.device_name"
|
||||||
|
go_type:
|
||||||
|
type: "string"
|
||||||
|
- column: "devices.sync"
|
||||||
|
go_type:
|
||||||
|
type: "bool"
|
||||||
|
- column: "devices.created_at"
|
||||||
|
go_type:
|
||||||
|
type: "string"
|
||||||
|
|
||||||
|
# Devices
|
||||||
|
- column: "users.pass"
|
||||||
|
go_type:
|
||||||
|
type: "string"
|
||||||
|
pointer: true
|
||||||
|
- column: "users.time_offset"
|
||||||
|
go_type:
|
||||||
|
type: "string"
|
||||||
|
pointer: true
|
||||||
|
|
||||||
# Do not generate JSON
|
# Do not generate JSON
|
||||||
- column: "documents.synced"
|
- column: "documents.synced"
|
||||||
go_struct_tag: 'json:"-"'
|
go_struct_tag: 'json:"-"'
|
||||||
|
@ -2,22 +2,24 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<link rel="manifest" href="{{ .RelBase }}./manifest.json" />
|
<link rel="manifest" href="{{ .RelBase }}./manifest.json" />
|
||||||
|
<meta name="theme-color" content="#F3F4F6" media="(prefers-color-scheme: light)">
|
||||||
|
<meta name="theme-color" content="#1F2937" media="(prefers-color-scheme: dark)">
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport"
|
<meta name="viewport"
|
||||||
content="width=device-width, initial-scale=0.90, user-scalable=no">
|
content="width=device-width, initial-scale=0.90, user-scalable=no">
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
<title>Book Manager - {{block "title" .}}{{end}}</title>
|
<title>Book Manager - {{block "title" .}}{{end}}</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body class="bg-gray-100 dark:bg-gray-800">
|
||||||
<main
|
<main
|
||||||
class="relative h-screen overflow-hidden bg-gray-100 dark:bg-gray-800"
|
class="relative h-screen overflow-hidden"
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-between w-full h-16">
|
<div class="flex items-center justify-between w-full h-16">
|
||||||
<div id="mobile-nav-button" class="flex flex-col z-40 relative ml-6">
|
<div id="mobile-nav-button" class="flex flex-col z-40 relative ml-6">
|
||||||
<input type="checkbox" class="absolute lg:hidden z-50 -top-2 w-7 h-7 flex cursor-pointer opacity-0 w-12 h-12" />
|
<input type="checkbox" class="absolute lg:hidden z-50 -top-2 w-7 h-7 flex cursor-pointer opacity-0 w-12 h-12" />
|
||||||
<span class="lg:hidden w-7 h-0.5 z-40 mt-0.5 bg-white"></span>
|
<span class="lg:hidden bg-black w-7 h-0.5 z-40 mt-0.5 dark:bg-white"></span>
|
||||||
<span class="lg:hidden w-7 h-0.5 z-40 mt-1 bg-white"></span>
|
<span class="lg:hidden bg-black w-7 h-0.5 z-40 mt-1 dark:bg-white"></span>
|
||||||
<span class="lg:hidden w-7 h-0.5 z-40 mt-1 bg-white"></span>
|
<span class="lg:hidden bg-black w-7 h-0.5 z-40 mt-1 dark:bg-white"></span>
|
||||||
|
|
||||||
<div id="menu" class="fixed -mt-6 -ml-6 lg:-mt-8 h-full w-56 lg:w-48 bg-white dark:bg-gray-700 shadow-lg">
|
<div id="menu" class="fixed -mt-6 -ml-6 lg:-mt-8 h-full w-56 lg:w-48 bg-white dark:bg-gray-700 shadow-lg">
|
||||||
<div class="h-16 flex justify-end lg:justify-around">
|
<div class="h-16 flex justify-end lg:justify-around">
|
||||||
@ -102,7 +104,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h1 class="text-xl font-bold dark:text-white px-6">{{block "header" .}}{{end}}</h1>
|
<h1 class="text-xl font-bold dark:text-white px-6 lg:ml-44">{{block "header" .}}{{end}}</h1>
|
||||||
<div
|
<div
|
||||||
class="relative flex items-center justify-end w-full p-4 space-x-4"
|
class="relative flex items-center justify-end w-full p-4 space-x-4"
|
||||||
>
|
>
|
||||||
@ -134,6 +136,15 @@
|
|||||||
aria-orientation="vertical"
|
aria-orientation="vertical"
|
||||||
aria-labelledby="options-menu"
|
aria-labelledby="options-menu"
|
||||||
>
|
>
|
||||||
|
<a
|
||||||
|
href="/settings"
|
||||||
|
class="block block px-4 py-2 text-md text-gray-700 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600"
|
||||||
|
role="menuitem"
|
||||||
|
>
|
||||||
|
<span class="flex flex-col">
|
||||||
|
<span>Settings</span>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
<a
|
<a
|
||||||
href="/logout"
|
href="/logout"
|
||||||
class="block block px-4 py-2 text-md text-gray-700 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600"
|
class="block block px-4 py-2 text-md text-gray-700 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600"
|
||||||
@ -175,7 +186,6 @@
|
|||||||
|
|
||||||
<!-- Custom Animation CSS -->
|
<!-- Custom Animation CSS -->
|
||||||
<style>
|
<style>
|
||||||
|
|
||||||
/* ----------------------------- */
|
/* ----------------------------- */
|
||||||
/* ------- User Dropdown ------- */
|
/* ------- User Dropdown ------- */
|
||||||
/* ----------------------------- */
|
/* ----------------------------- */
|
||||||
@ -210,7 +220,6 @@
|
|||||||
#mobile-nav-button input:checked ~ span {
|
#mobile-nav-button input:checked ~ span {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: rotate(45deg) translate(2px, -2px);
|
transform: rotate(45deg) translate(2px, -2px);
|
||||||
background: #FFFFFF;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#mobile-nav-button input:checked ~ span:nth-last-child(3) {
|
#mobile-nav-button input:checked ~ span:nth-last-child(3) {
|
||||||
|
@ -2,9 +2,20 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<link rel="manifest" href="./manifest.json" />
|
<link rel="manifest" href="./manifest.json" />
|
||||||
|
<meta
|
||||||
|
name="theme-color"
|
||||||
|
content="#F3F4F6"
|
||||||
|
media="(prefers-color-scheme: light)"
|
||||||
|
/>
|
||||||
|
<meta
|
||||||
|
name="theme-color"
|
||||||
|
content="#1F2937"
|
||||||
|
media="(prefers-color-scheme: dark)"
|
||||||
|
/>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width" />
|
<meta name="viewport" content="width=device-width" />
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<title>Book Manager - {{if .Register}}Register{{else}}Login{{end}}</title>
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-gray-100 dark:bg-gray-800 dark:text-white">
|
<body class="bg-gray-100 dark:bg-gray-800 dark:text-white">
|
||||||
<div class="flex flex-wrap w-full">
|
<div class="flex flex-wrap w-full">
|
||||||
|
217
templates/settings.html
Normal file
217
templates/settings.html
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
{{template "base.html" .}} {{define "title"}}Settings{{end}} {{define "header"}}
|
||||||
|
<a href="./settings">Settings</a>
|
||||||
|
{{end}} {{define "content"}}
|
||||||
|
<div class="h-full w-full relative">
|
||||||
|
<div class="w-full flex flex-col md:flex-row gap-4">
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="flex flex-col p-4 items-center rounded shadow-lg md:w-60 lg:w-80 bg-white dark:bg-gray-700 text-gray-500 dark:text-white"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="60"
|
||||||
|
fill="currentColor"
|
||||||
|
height="60"
|
||||||
|
class="text-gray-800 dark:text-gray-200"
|
||||||
|
viewBox="0 0 1792 1792"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M1523 1339q-22-155-87.5-257.5t-184.5-118.5q-67 74-159.5 115.5t-195.5 41.5-195.5-41.5-159.5-115.5q-119 16-184.5 118.5t-87.5 257.5q106 150 271 237.5t356 87.5 356-87.5 271-237.5zm-243-699q0-159-112.5-271.5t-271.5-112.5-271.5 112.5-112.5 271.5 112.5 271.5 271.5 112.5 271.5-112.5 112.5-271.5zm512 256q0 182-71 347.5t-190.5 286-285.5 191.5-349 71q-182 0-348-71t-286-191-191-286-71-348 71-348 191-286 286-191 348-71 348 71 286 191 191 286 71 348z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<p class="text-lg">{{ .User }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-4 grow">
|
||||||
|
<div
|
||||||
|
class="flex flex-col gap-2 grow p-4 rounded shadow-lg bg-white dark:bg-gray-700 text-gray-500 dark:text-white"
|
||||||
|
>
|
||||||
|
<p class="text-lg font-semibold mb-2">Change Password</p>
|
||||||
|
<form
|
||||||
|
class="flex gap-4 flex-col lg:flex-row"
|
||||||
|
action="./settings"
|
||||||
|
method="POST"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col grow">
|
||||||
|
<div class="flex relative">
|
||||||
|
<span
|
||||||
|
class="inline-flex items-center px-3 border-t bg-white border-l border-b border-gray-300 text-gray-500 shadow-sm text-sm"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="15"
|
||||||
|
height="15"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 1792 1792"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M1376 768q40 0 68 28t28 68v576q0 40-28 68t-68 28h-960q-40 0-68-28t-28-68v-576q0-40 28-68t68-28h32v-320q0-185 131.5-316.5t316.5-131.5 316.5 131.5 131.5 316.5q0 26-19 45t-45 19h-64q-26 0-45-19t-19-45q0-106-75-181t-181-75-181 75-75 181v320h736z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
class="flex-1 appearance-none rounded-none border border-gray-300 w-full py-2 px-4 bg-white text-gray-700 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-transparent"
|
||||||
|
placeholder="Password"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col grow">
|
||||||
|
<div class="flex relative">
|
||||||
|
<span
|
||||||
|
class="inline-flex items-center px-3 border-t bg-white border-l border-b border-gray-300 text-gray-500 shadow-sm text-sm"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="15"
|
||||||
|
height="15"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 1792 1792"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M1376 768q40 0 68 28t28 68v576q0 40-28 68t-68 28h-960q-40 0-68-28t-28-68v-576q0-40 28-68t68-28h32v-320q0-185 131.5-316.5t316.5-131.5 316.5 131.5 131.5 316.5q0 26-19 45t-45 19h-64q-26 0-45-19t-19-45q0-106-75-181t-181-75-181 75-75 181v320h736z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
id="new_password"
|
||||||
|
name="new_password"
|
||||||
|
class="flex-1 appearance-none rounded-none border border-gray-300 w-full py-2 px-4 bg-white text-gray-700 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-transparent"
|
||||||
|
placeholder="New Password"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="px-10 py-2 text-base font-semibold text-center text-white transition duration-200 ease-in bg-black shadow-md hover:text-black hover:bg-white focus:outline-none focus:ring-2"
|
||||||
|
>
|
||||||
|
<span class="w-full">Submit</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{{ if .PasswordErrorMessage }}
|
||||||
|
<span class="text-red-400 text-xs">{{ .PasswordErrorMessage }}</span>
|
||||||
|
{{ else if .PasswordMessage }}
|
||||||
|
<span class="text-green-400 text-xs">{{ .PasswordMessage }}</span>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="flex flex-col grow gap-2 p-4 rounded shadow-lg bg-white dark:bg-gray-700 text-gray-500 dark:text-white"
|
||||||
|
>
|
||||||
|
<p class="text-lg font-semibold mb-2">Change Time Offset</p>
|
||||||
|
<form
|
||||||
|
class="flex gap-4 flex-col lg:flex-row"
|
||||||
|
action="./settings"
|
||||||
|
method="POST"
|
||||||
|
>
|
||||||
|
<div class="flex relative grow">
|
||||||
|
<span
|
||||||
|
class="inline-flex items-center px-3 border-t bg-white border-l border-b border-gray-300 text-gray-500 shadow-sm text-sm"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
d="M12 7.25C12.4142 7.25 12.75 7.58579 12.75 8V11.6893L15.0303 13.9697C15.3232 14.2626 15.3232 14.7374 15.0303 15.0303C14.7374 15.3232 14.2626 15.3232 13.9697 15.0303L11.4697 12.5303C11.329 12.3897 11.25 12.1989 11.25 12V8C11.25 7.58579 11.5858 7.25 12 7.25Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<select
|
||||||
|
class="flex-1 appearance-none rounded-none border border-gray-300 w-full py-2 px-4 bg-white text-gray-700 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-transparent"
|
||||||
|
id="time_offset"
|
||||||
|
name="time_offset"
|
||||||
|
>
|
||||||
|
{{ range $item := GetUTCOffsets }}
|
||||||
|
<option
|
||||||
|
{{
|
||||||
|
if
|
||||||
|
(eq
|
||||||
|
$item.Value
|
||||||
|
$.Data.Settings.TimeOffset)
|
||||||
|
}}selected{{
|
||||||
|
end
|
||||||
|
}}
|
||||||
|
value="{{ $item.Value }}"
|
||||||
|
>
|
||||||
|
{{ $item.Name }}
|
||||||
|
</option>
|
||||||
|
{{ end }}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="px-10 py-2 text-base font-semibold text-center text-white transition duration-200 ease-in bg-black shadow-md hover:text-black hover:bg-white focus:outline-none focus:ring-2"
|
||||||
|
>
|
||||||
|
<span class="w-full">Submit</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{{ if .TimeOffsetErrorMessage }}
|
||||||
|
<span class="text-red-400 text-xs">{{ .TimeOffsetErrorMessage }}</span>
|
||||||
|
{{ else if .TimeOffsetMessage }}
|
||||||
|
<span class="text-green-400 text-xs">{{ .TimeOffsetMessage }}</span>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="flex flex-col grow p-4 rounded shadow-lg bg-white dark:bg-gray-700 text-gray-500 dark:text-white"
|
||||||
|
>
|
||||||
|
<p class="text-lg font-semibold">Devices</p>
|
||||||
|
<table class="min-w-full bg-white dark:bg-gray-700 text-sm">
|
||||||
|
<thead class="text-gray-800 dark:text-gray-400">
|
||||||
|
<tr>
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
class="p-3 pl-0 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800"
|
||||||
|
>
|
||||||
|
Name
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800"
|
||||||
|
>
|
||||||
|
Last Sync
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800"
|
||||||
|
>
|
||||||
|
Created
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="text-black dark:text-white">
|
||||||
|
{{ range $device := .Data.Devices }}
|
||||||
|
<tr>
|
||||||
|
<td class="p-3 pl-0">
|
||||||
|
<p>{{ $device.DeviceName }}</p>
|
||||||
|
</td>
|
||||||
|
<td class="p-3">
|
||||||
|
<p>{{ $device.LastSync }}</p>
|
||||||
|
</td>
|
||||||
|
<td class="p-3">
|
||||||
|
<p>{{ $device.CreatedAt }}</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{ end }}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
@ -1,49 +1,51 @@
|
|||||||
package utils
|
package utils
|
||||||
|
|
||||||
import (
|
type UTCOffset struct {
|
||||||
"bytes"
|
Name string
|
||||||
"crypto/md5"
|
Value string
|
||||||
// "encoding/hex"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
func CalculatePartialMD5(filePath string) string {
|
|
||||||
file, err := os.Open(filePath)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
var step int64 = 1024
|
|
||||||
var size int64 = 1024
|
|
||||||
var buf bytes.Buffer
|
|
||||||
|
|
||||||
for i := -1; i <= 10; i++ {
|
|
||||||
byteStep := make([]byte, size)
|
|
||||||
|
|
||||||
var newShift int64 = int64(i * 2)
|
|
||||||
var newOffset int64
|
|
||||||
if i == -1 {
|
|
||||||
newOffset = 0
|
|
||||||
} else {
|
|
||||||
newOffset = step << newShift
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := file.ReadAt(byteStep, newOffset)
|
|
||||||
if err == io.EOF {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
buf.Write(byteStep)
|
|
||||||
}
|
|
||||||
|
|
||||||
allBytes := buf.Bytes()
|
|
||||||
return fmt.Sprintf("%x", md5.Sum(allBytes))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
var UTC_OFFSETS = []UTCOffset{
|
||||||
fileHash := CalculatePartialMD5("test.epub")
|
{Value: "-12 hours", Name: "UTC−12:00"},
|
||||||
fmt.Println("MD5: ", fileHash)
|
{Value: "-11 hours", Name: "UTC−11:00"},
|
||||||
|
{Value: "-10 hours", Name: "UTC−10:00"},
|
||||||
|
{Value: "-9.5 hours", Name: "UTC−09:30"},
|
||||||
|
{Value: "-9 hours", Name: "UTC−09:00"},
|
||||||
|
{Value: "-8 hours", Name: "UTC−08:00"},
|
||||||
|
{Value: "-7 hours", Name: "UTC−07:00"},
|
||||||
|
{Value: "-6 hours", Name: "UTC−06:00"},
|
||||||
|
{Value: "-5 hours", Name: "UTC−05:00"},
|
||||||
|
{Value: "-4 hours", Name: "UTC−04:00"},
|
||||||
|
{Value: "-3.5 hours", Name: "UTC−03:30"},
|
||||||
|
{Value: "-3 hours", Name: "UTC−03:00"},
|
||||||
|
{Value: "-2 hours", Name: "UTC−02:00"},
|
||||||
|
{Value: "-1 hours", Name: "UTC−01:00"},
|
||||||
|
{Value: "0 hours", Name: "UTC±00:00"},
|
||||||
|
{Value: "+1 hours", Name: "UTC+01:00"},
|
||||||
|
{Value: "+2 hours", Name: "UTC+02:00"},
|
||||||
|
{Value: "+3 hours", Name: "UTC+03:00"},
|
||||||
|
{Value: "+3.5 hours", Name: "UTC+03:30"},
|
||||||
|
{Value: "+4 hours", Name: "UTC+04:00"},
|
||||||
|
{Value: "+4.5 hours", Name: "UTC+04:30"},
|
||||||
|
{Value: "+5 hours", Name: "UTC+05:00"},
|
||||||
|
{Value: "+5.5 hours", Name: "UTC+05:30"},
|
||||||
|
{Value: "+5.75 hours", Name: "UTC+05:45"},
|
||||||
|
{Value: "+6 hours", Name: "UTC+06:00"},
|
||||||
|
{Value: "+6.5 hours", Name: "UTC+06:30"},
|
||||||
|
{Value: "+7 hours", Name: "UTC+07:00"},
|
||||||
|
{Value: "+8 hours", Name: "UTC+08:00"},
|
||||||
|
{Value: "+8.75 hours", Name: "UTC+08:45"},
|
||||||
|
{Value: "+9 hours", Name: "UTC+09:00"},
|
||||||
|
{Value: "+9.5 hours", Name: "UTC+09:30"},
|
||||||
|
{Value: "+10 hours", Name: "UTC+10:00"},
|
||||||
|
{Value: "+10.5 hours", Name: "UTC+10:30"},
|
||||||
|
{Value: "+11 hours", Name: "UTC+11:00"},
|
||||||
|
{Value: "+12 hours", Name: "UTC+12:00"},
|
||||||
|
{Value: "+12.75 hours", Name: "UTC+12:45"},
|
||||||
|
{Value: "+13 hours", Name: "UTC+13:00"},
|
||||||
|
{Value: "+14 hours", Name: "UTC+14:00"},
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetUTCOffsets() []UTCOffset {
|
||||||
|
return UTC_OFFSETS
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user