mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-07 22:06:38 +08:00
Fix logo assets and license heartbeat
This commit is contained in:
@@ -120,6 +120,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
go services.Boot()
|
go services.Boot()
|
||||||
|
go handler.RunLicenseHeartbeatLoop(services.Context(), services)
|
||||||
go services.TelegramBot.StartPolling(context.Background())
|
go services.TelegramBot.StartPolling(context.Background())
|
||||||
|
|
||||||
// Graceful shutdown.
|
// Graceful shutdown.
|
||||||
@@ -201,6 +202,12 @@ func serveSPA(r *gin.Engine, webDir string) {
|
|||||||
c.Next()
|
c.Next()
|
||||||
})
|
})
|
||||||
assets.Static("/", filepath.Join(webDir, "assets"))
|
assets.Static("/", filepath.Join(webDir, "assets"))
|
||||||
|
brand := r.Group("/brand")
|
||||||
|
brand.Use(func(c *gin.Context) {
|
||||||
|
c.Header("Cache-Control", "public, max-age=86400")
|
||||||
|
c.Next()
|
||||||
|
})
|
||||||
|
brand.Static("/", filepath.Join(webDir, "brand"))
|
||||||
for _, icon := range []string{"/favicon.ico", "/favicon.svg"} {
|
for _, icon := range []string{"/favicon.ico", "/favicon.svg"} {
|
||||||
iconPath := filepath.Join(webDir, strings.TrimPrefix(icon, "/"))
|
iconPath := filepath.Join(webDir, strings.TrimPrefix(icon, "/"))
|
||||||
r.GET(icon, serveNoCacheFile(iconPath))
|
r.GET(icon, serveNoCacheFile(iconPath))
|
||||||
|
|||||||
@@ -52,12 +52,18 @@ func TestServeSPAServesAssetsImmutableAndBypassesAPIRoutes(t *testing.T) {
|
|||||||
if err := os.MkdirAll(filepath.Join(webDir, "assets"), 0o755); err != nil {
|
if err := os.MkdirAll(filepath.Join(webDir, "assets"), 0o755); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
if err := os.MkdirAll(filepath.Join(webDir, "brand"), 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
if err := os.WriteFile(filepath.Join(webDir, "index.html"), []byte("index"), 0o644); err != nil {
|
if err := os.WriteFile(filepath.Join(webDir, "index.html"), []byte("index"), 0o644); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(filepath.Join(webDir, "assets", "app.js"), []byte("console.log('ok')"), 0o644); err != nil {
|
if err := os.WriteFile(filepath.Join(webDir, "assets", "app.js"), []byte("console.log('ok')"), 0o644); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(webDir, "brand", "mediastationgo-logo.svg"), []byte("<svg></svg>"), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
router := gin.New()
|
router := gin.New()
|
||||||
serveSPA(router, webDir)
|
serveSPA(router, webDir)
|
||||||
@@ -72,6 +78,16 @@ func TestServeSPAServesAssetsImmutableAndBypassesAPIRoutes(t *testing.T) {
|
|||||||
t.Fatalf("asset Cache-Control = %q, want immutable", got)
|
t.Fatalf("asset Cache-Control = %q, want immutable", got)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
brandReq := httptest.NewRequest(http.MethodGet, "/brand/mediastationgo-logo.svg", nil)
|
||||||
|
brandResp := httptest.NewRecorder()
|
||||||
|
router.ServeHTTP(brandResp, brandReq)
|
||||||
|
if brandResp.Code != http.StatusOK {
|
||||||
|
t.Fatalf("brand asset status = %d, want 200", brandResp.Code)
|
||||||
|
}
|
||||||
|
if strings.Contains(brandResp.Body.String(), "index") {
|
||||||
|
t.Fatalf("brand asset should not serve SPA index: %q", brandResp.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
for _, path := range []string{
|
for _, path := range []string{
|
||||||
"/api/missing",
|
"/api/missing",
|
||||||
"/emby",
|
"/emby",
|
||||||
|
|||||||
+112
-27
@@ -18,6 +18,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
"github.com/ShukeBta/MediaStationGo/internal/service"
|
||||||
)
|
)
|
||||||
@@ -27,6 +28,10 @@ const (
|
|||||||
licenseHMACSecretSetting = "license.hmac_secret" // #nosec G101 -- setting key name, not the HMAC secret value.
|
licenseHMACSecretSetting = "license.hmac_secret" // #nosec G101 -- setting key name, not the HMAC secret value.
|
||||||
licenseDeviceIDSetting = "license.device_id"
|
licenseDeviceIDSetting = "license.device_id"
|
||||||
licenseDeviceNameSetting = "license.device_name"
|
licenseDeviceNameSetting = "license.device_name"
|
||||||
|
|
||||||
|
licenseHeartbeatInterval = 12 * time.Hour
|
||||||
|
licenseHeartbeatCheckInterval = 30 * time.Minute
|
||||||
|
licenseHeartbeatStartupDelay = 2 * time.Minute
|
||||||
)
|
)
|
||||||
|
|
||||||
type licenseActivateReq struct {
|
type licenseActivateReq struct {
|
||||||
@@ -150,41 +155,121 @@ func licenseStatusHandler(svc *service.Container) gin.HandlerFunc {
|
|||||||
|
|
||||||
func licenseHeartbeatHandler(svc *service.Container) gin.HandlerFunc {
|
func licenseHeartbeatHandler(svc *service.Container) gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
client, err := newLicenseClient(c.Request.Context(), svc)
|
state, err := sendLicenseHeartbeat(c.Request.Context(), svc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
oldState, _ := loadLicenseState(c.Request.Context(), svc)
|
|
||||||
deviceID, err := ensureLicenseDeviceID(c.Request.Context(), svc, oldState.DeviceID)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
deviceName, _ := svc.Repo.Setting.Get(c.Request.Context(), licenseDeviceNameSetting)
|
|
||||||
if strings.TrimSpace(deviceName) == "" {
|
|
||||||
deviceName = defaultLicenseDeviceName()
|
|
||||||
_ = svc.Repo.Setting.Set(c.Request.Context(), licenseDeviceNameSetting, deviceName)
|
|
||||||
}
|
|
||||||
var upstream licenseServerSignedResp
|
|
||||||
if err := client.post(c.Request.Context(), "/api/v1/heartbeat", licenseHeartbeatPayload(oldState, deviceID, deviceName), &upstream); err != nil {
|
|
||||||
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
|
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := client.verifySigned(&upstream); err != nil {
|
|
||||||
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
state := licenseStateFromSigned(upstream, deviceID, deviceName)
|
|
||||||
state.LicenseKey = oldState.LicenseKey
|
|
||||||
if err := persistLicenseState(c.Request.Context(), svc, state); err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(http.StatusOK, licenseActivationView(state))
|
c.JSON(http.StatusOK, licenseActivationView(state))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RunLicenseHeartbeatLoop keeps the license server aware of active deployments.
|
||||||
|
// The loop checks periodically, but only sends when the last stored heartbeat is
|
||||||
|
// older than licenseHeartbeatInterval.
|
||||||
|
func RunLicenseHeartbeatLoop(ctx context.Context, svc *service.Container) {
|
||||||
|
if svc == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
run := func() {
|
||||||
|
state, sent, err := maybeSendLicenseHeartbeat(ctx, svc, licenseHeartbeatInterval)
|
||||||
|
if err != nil {
|
||||||
|
if svc.Log != nil {
|
||||||
|
svc.Log.Warn("license heartbeat failed", zap.Error(err))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if sent && svc.Log != nil {
|
||||||
|
svc.Log.Info("license heartbeat sent", zap.String("device_id", state.DeviceID))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
timer := time.NewTimer(licenseHeartbeatStartupDelay)
|
||||||
|
defer timer.Stop()
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-timer.C:
|
||||||
|
run()
|
||||||
|
}
|
||||||
|
|
||||||
|
ticker := time.NewTicker(licenseHeartbeatCheckInterval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
run()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func maybeSendLicenseHeartbeat(ctx context.Context, svc *service.Container, interval time.Duration) (service.LicenseActivationState, bool, error) {
|
||||||
|
state, err := loadLicenseState(ctx, svc)
|
||||||
|
if err != nil {
|
||||||
|
return state, false, nil
|
||||||
|
}
|
||||||
|
if !licenseHeartbeatEligible(state) || !licenseHeartbeatDue(state, interval) {
|
||||||
|
return state, false, nil
|
||||||
|
}
|
||||||
|
next, err := sendLicenseHeartbeat(ctx, svc)
|
||||||
|
if err != nil {
|
||||||
|
return state, false, err
|
||||||
|
}
|
||||||
|
return next, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func licenseHeartbeatEligible(state service.LicenseActivationState) bool {
|
||||||
|
return strings.TrimSpace(state.LicenseKey) != "" || state.Valid
|
||||||
|
}
|
||||||
|
|
||||||
|
func licenseHeartbeatDue(state service.LicenseActivationState, interval time.Duration) bool {
|
||||||
|
if interval <= 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
updatedAt := strings.TrimSpace(state.UpdatedAt)
|
||||||
|
if updatedAt == "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, layout := range []string{time.RFC3339, "2006-01-02 15:04:05", "2006-01-02"} {
|
||||||
|
if t, err := time.Parse(layout, updatedAt); err == nil {
|
||||||
|
return time.Since(t) >= interval
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendLicenseHeartbeat(ctx context.Context, svc *service.Container) (service.LicenseActivationState, error) {
|
||||||
|
client, err := newLicenseClient(ctx, svc)
|
||||||
|
if err != nil {
|
||||||
|
return service.LicenseActivationState{}, err
|
||||||
|
}
|
||||||
|
oldState, _ := loadLicenseState(ctx, svc)
|
||||||
|
deviceID, err := ensureLicenseDeviceID(ctx, svc, oldState.DeviceID)
|
||||||
|
if err != nil {
|
||||||
|
return service.LicenseActivationState{}, err
|
||||||
|
}
|
||||||
|
deviceName, _ := svc.Repo.Setting.Get(ctx, licenseDeviceNameSetting)
|
||||||
|
if strings.TrimSpace(deviceName) == "" {
|
||||||
|
deviceName = defaultLicenseDeviceName()
|
||||||
|
_ = svc.Repo.Setting.Set(ctx, licenseDeviceNameSetting, deviceName)
|
||||||
|
}
|
||||||
|
var upstream licenseServerSignedResp
|
||||||
|
if err := client.post(ctx, "/api/v1/heartbeat", licenseHeartbeatPayload(oldState, deviceID, deviceName), &upstream); err != nil {
|
||||||
|
return service.LicenseActivationState{}, err
|
||||||
|
}
|
||||||
|
if err := client.verifySigned(&upstream); err != nil {
|
||||||
|
return service.LicenseActivationState{}, err
|
||||||
|
}
|
||||||
|
state := licenseStateFromSigned(upstream, deviceID, deviceName)
|
||||||
|
state.LicenseKey = oldState.LicenseKey
|
||||||
|
if err := persistLicenseState(ctx, svc, state); err != nil {
|
||||||
|
return service.LicenseActivationState{}, err
|
||||||
|
}
|
||||||
|
return state, nil
|
||||||
|
}
|
||||||
|
|
||||||
type licenseClient struct {
|
type licenseClient struct {
|
||||||
baseURL string
|
baseURL string
|
||||||
hmacSecret string
|
hmacSecret string
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package handler
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
"github.com/ShukeBta/MediaStationGo/internal/service"
|
||||||
)
|
)
|
||||||
@@ -90,3 +91,30 @@ func TestLicenseHeartbeatPayloadIncludesStoredLicenseKey(t *testing.T) {
|
|||||||
t.Fatalf("heartbeat should include stored license key for server-side backfill: %#v", payload)
|
t.Fatalf("heartbeat should include stored license key for server-side backfill: %#v", payload)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLicenseHeartbeatDueUsesTwelveHourWindow(t *testing.T) {
|
||||||
|
state := service.LicenseActivationState{
|
||||||
|
Valid: true,
|
||||||
|
UpdatedAt: time.Now().Add(-11 * time.Hour).Format(time.RFC3339),
|
||||||
|
}
|
||||||
|
if licenseHeartbeatDue(state, 12*time.Hour) {
|
||||||
|
t.Fatalf("heartbeat should not be due before interval")
|
||||||
|
}
|
||||||
|
|
||||||
|
state.UpdatedAt = time.Now().Add(-13 * time.Hour).Format(time.RFC3339)
|
||||||
|
if !licenseHeartbeatDue(state, 12*time.Hour) {
|
||||||
|
t.Fatalf("heartbeat should be due after interval")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLicenseHeartbeatEligibleRequiresActivationState(t *testing.T) {
|
||||||
|
if licenseHeartbeatEligible(service.LicenseActivationState{DeviceID: "device-only"}) {
|
||||||
|
t.Fatalf("device id alone should not trigger automatic license heartbeat")
|
||||||
|
}
|
||||||
|
if !licenseHeartbeatEligible(service.LicenseActivationState{LicenseKey: "MS-KEY"}) {
|
||||||
|
t.Fatalf("stored license key should trigger automatic license heartbeat")
|
||||||
|
}
|
||||||
|
if !licenseHeartbeatEligible(service.LicenseActivationState{Valid: true}) {
|
||||||
|
t.Fatalf("valid license state should trigger automatic license heartbeat")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -420,6 +420,14 @@ func (c *Container) NormalizeCloudLibraryTypes(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Context is canceled when the service container is closing.
|
||||||
|
func (c *Container) Context() context.Context {
|
||||||
|
if c == nil || c.stopCtx == nil {
|
||||||
|
return context.Background()
|
||||||
|
}
|
||||||
|
return c.stopCtx
|
||||||
|
}
|
||||||
|
|
||||||
// runInactivitySweeper periodically runs the account-cleanup policy. Kept with
|
// runInactivitySweeper periodically runs the account-cleanup policy. Kept with
|
||||||
// the historical name to avoid churn in callers.
|
// the historical name to avoid churn in callers.
|
||||||
func (c *Container) runInactivitySweeper(ctx context.Context) {
|
func (c *Container) runInactivitySweeper(ctx context.Context) {
|
||||||
|
|||||||
+2
-2
@@ -5,8 +5,8 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta name="theme-color" content="#faf8f5" />
|
<meta name="theme-color" content="#faf8f5" />
|
||||||
<meta name="description" content="MediaStationGo — 个人家庭媒体中心" />
|
<meta name="description" content="MediaStationGo — 个人家庭媒体中心" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/brand/mgo-emby-icon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/brand/mediastationgo-logo.svg" />
|
||||||
<link rel="apple-touch-icon" href="/brand/mgo-emby-icon.svg" />
|
<link rel="apple-touch-icon" href="/brand/mediastationgo-logo.svg" />
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link
|
<link
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="MediaStationGo">
|
||||||
|
<rect width="64" height="64" rx="16" fill="url(#mgoPurpleGrad)" />
|
||||||
|
<path d="M22 20 L42 32 L22 44" stroke="white" stroke-width="5" stroke-linecap="square" stroke-linejoin="miter" fill="none" />
|
||||||
|
<path d="M22 36 L22 48 L32 42" stroke="white" stroke-width="5" stroke-linecap="square" stroke-linejoin="miter" fill="none" />
|
||||||
|
<circle cx="46" cy="18" r="4" fill="#06B6D4" />
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="mgoPurpleGrad" x1="0" y1="0" x2="64" y2="64" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#7C3AED" />
|
||||||
|
<stop offset="1" stop-color="#5B21B6" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 742 B |
@@ -1,10 +1,10 @@
|
|||||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="MediaStationGo">
|
||||||
<rect width="32" height="32" rx="8" fill="url(#mgoPurpleGrad)" />
|
<rect width="64" height="64" rx="16" fill="url(#mgoPurpleGrad)" />
|
||||||
<path d="M11 10 L21 16 L11 22" stroke="white" stroke-width="2.5" stroke-linecap="square" stroke-linejoin="miter" fill="none" />
|
<path d="M22 20 L42 32 L22 44" stroke="white" stroke-width="5" stroke-linecap="square" stroke-linejoin="miter" fill="none" />
|
||||||
<path d="M11 18 L11 24 L16 21" stroke="white" stroke-width="2.5" stroke-linecap="square" stroke-linejoin="miter" fill="none" />
|
<path d="M22 36 L22 48 L32 42" stroke="white" stroke-width="5" stroke-linecap="square" stroke-linejoin="miter" fill="none" />
|
||||||
<circle cx="23" cy="9" r="2" fill="#06B6D4" />
|
<circle cx="46" cy="18" r="4" fill="#06B6D4" />
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient id="mgoPurpleGrad" x1="0" y1="0" x2="32" y2="32" gradientUnits="userSpaceOnUse">
|
<linearGradient id="mgoPurpleGrad" x1="0" y1="0" x2="64" y2="64" gradientUnits="userSpaceOnUse">
|
||||||
<stop stop-color="#7C3AED" />
|
<stop stop-color="#7C3AED" />
|
||||||
<stop offset="1" stop-color="#5B21B6" />
|
<stop offset="1" stop-color="#5B21B6" />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 705 B After Width: | Height: | Size: 742 B |
@@ -1,10 +1,10 @@
|
|||||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="MediaStationGo">
|
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="MediaStationGo">
|
||||||
<rect width="32" height="32" rx="8" fill="url(#mgoPurpleGrad)" />
|
<rect width="64" height="64" rx="16" fill="url(#mgoPurpleGrad)" />
|
||||||
<path d="M11 10 L21 16 L11 22" stroke="white" stroke-width="2.5" stroke-linecap="square" stroke-linejoin="miter" fill="none" />
|
<path d="M22 20 L42 32 L22 44" stroke="white" stroke-width="5" stroke-linecap="square" stroke-linejoin="miter" fill="none" />
|
||||||
<path d="M11 18 L11 24 L16 21" stroke="white" stroke-width="2.5" stroke-linecap="square" stroke-linejoin="miter" fill="none" />
|
<path d="M22 36 L22 48 L32 42" stroke="white" stroke-width="5" stroke-linecap="square" stroke-linejoin="miter" fill="none" />
|
||||||
<circle cx="23" cy="9" r="2" fill="#06B6D4" />
|
<circle cx="46" cy="18" r="4" fill="#06B6D4" />
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient id="mgoPurpleGrad" x1="0" y1="0" x2="32" y2="32" gradientUnits="userSpaceOnUse">
|
<linearGradient id="mgoPurpleGrad" x1="0" y1="0" x2="64" y2="64" gradientUnits="userSpaceOnUse">
|
||||||
<stop stop-color="#7C3AED" />
|
<stop stop-color="#7C3AED" />
|
||||||
<stop offset="1" stop-color="#5B21B6" />
|
<stop offset="1" stop-color="#5B21B6" />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 744 B After Width: | Height: | Size: 742 B |
@@ -192,7 +192,7 @@ export function Layout() {
|
|||||||
<div className="flex h-20 items-center justify-between px-6 border-b border-gray-100">
|
<div className="flex h-20 items-center justify-between px-6 border-b border-gray-100">
|
||||||
<Link to="/" className="flex items-center gap-3">
|
<Link to="/" className="flex items-center gap-3">
|
||||||
<img
|
<img
|
||||||
src="/brand/mgo-emby-icon.svg"
|
src="/brand/mediastationgo-logo.svg"
|
||||||
alt="MediaStationGo"
|
alt="MediaStationGo"
|
||||||
className="h-10 w-10 shrink-0 rounded-xl object-contain shadow-sm"
|
className="h-10 w-10 shrink-0 rounded-xl object-contain shadow-sm"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export function LoginPage() {
|
|||||||
initial={{ scale: 0.8, opacity: 0 }}
|
initial={{ scale: 0.8, opacity: 0 }}
|
||||||
animate={{ scale: 1, opacity: 1 }}
|
animate={{ scale: 1, opacity: 1 }}
|
||||||
transition={{ delay: 0.15, type: 'spring', stiffness: 200 }}
|
transition={{ delay: 0.15, type: 'spring', stiffness: 200 }}
|
||||||
src="/brand/mgo-emby-icon.svg"
|
src="/brand/mediastationgo-logo.svg"
|
||||||
alt="MediaStationGo"
|
alt="MediaStationGo"
|
||||||
className="mb-4 h-14 w-14 rounded-2xl object-contain shadow-sm"
|
className="mb-4 h-14 w-14 rounded-2xl object-contain shadow-sm"
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user