+
+
+ {isWindows && }
)
diff --git a/src/app/observers/theme-observer.tsx b/src/app/observers/theme-observer.tsx
index 7e4fe316..4c344eb1 100644
--- a/src/app/observers/theme-observer.tsx
+++ b/src/app/observers/theme-observer.tsx
@@ -6,20 +6,31 @@ export function ThemeObserver() {
useEffect(() => {
const root = window.document.documentElement
+ const systemThemeQuery = window.matchMedia('(prefers-color-scheme: dark)')
- root.classList.remove('light', 'dark')
+ const updateTheme = () => {
+ root.classList.remove('light', 'dark')
- if (theme === 'system') {
- const systemTheme = window.matchMedia('(prefers-color-scheme: dark)')
- .matches
- ? 'dark'
- : 'light'
+ if (theme === 'system') {
+ const systemTheme = systemThemeQuery.matches ? 'dark' : 'light'
+ root.classList.add(systemTheme)
+ } else {
+ root.classList.add(theme)
+ }
+ }
+
+ // Atualizar o tema ao montar o componente
+ updateTheme()
- root.classList.add(systemTheme)
- return
+ // Escutar mudanças no tema do sistema
+ if (theme === 'system') {
+ systemThemeQuery.addEventListener('change', updateTheme)
}
- root.classList.add(theme)
+ // Limpeza do listener
+ return () => {
+ systemThemeQuery.removeEventListener('change', updateTheme)
+ }
}, [theme])
return null
diff --git a/src/app/observers/update-observer.tsx b/src/app/observers/update-observer.tsx
new file mode 100644
index 00000000..503af282
--- /dev/null
+++ b/src/app/observers/update-observer.tsx
@@ -0,0 +1,126 @@
+import { useQuery } from '@tanstack/react-query'
+import { relaunch } from '@tauri-apps/plugin-process'
+import { check } from '@tauri-apps/plugin-updater'
+import { Loader2, RocketIcon } from 'lucide-react'
+import { FormEvent, useEffect, useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import Markdown from 'react-markdown'
+import { toast } from 'react-toastify'
+import remarkGfm from 'remark-gfm'
+import {
+ AlertDialog,
+ AlertDialogContent,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+} from '@/app/components/ui/alert-dialog'
+import { Badge } from '@/app/components/ui/badge'
+import { Button } from '@/app/components/ui/button'
+import { useAppUpdate } from '@/store/app.store'
+import { queryKeys } from '@/utils/queryKeys'
+
+export function UpdateObserver() {
+ const { t } = useTranslation()
+ const { openDialog, setOpenDialog, remindOnNextBoot, setRemindOnNextBoot } =
+ useAppUpdate()
+ const [updateHasStarted, setUpdateHasStarted] = useState(false)
+
+ const { data: updateInfo } = useQuery({
+ queryKey: [queryKeys.update.check],
+ queryFn: () => check(),
+ enabled: !remindOnNextBoot,
+ refetchOnWindowFocus: false,
+ refetchOnMount: false,
+ staleTime: Infinity,
+ gcTime: Infinity,
+ })
+
+ useEffect(() => {
+ if (updateInfo) {
+ setOpenDialog(true)
+ }
+ }, [setOpenDialog, updateInfo])
+
+ if (!updateInfo || !updateInfo.available) return null
+
+ const handleUpdate = async (event: FormEvent
) => {
+ event.preventDefault()
+
+ toast(t('update.toasts.started'), {
+ autoClose: false,
+ type: 'default',
+ isLoading: true,
+ toastId: 'update',
+ })
+
+ try {
+ setUpdateHasStarted(true)
+ await updateInfo.downloadAndInstall()
+
+ toast.update('update', {
+ render: t('update.toasts.success'),
+ type: 'success',
+ autoClose: 5000,
+ isLoading: false,
+ })
+
+ await relaunch()
+ } catch (_) {
+ setUpdateHasStarted(false)
+ setRemindOnNextBoot(true)
+
+ toast.update('update', {
+ render: t('update.toasts.error'),
+ type: 'error',
+ autoClose: 5000,
+ isLoading: false,
+ })
+ }
+ }
+
+ return (
+
+
+
+
+
+ {t('update.dialog.title')}
+ {updateInfo.version}
+
+
+
+
+
+ {updateInfo.body}
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/app/pages/login.tsx b/src/app/pages/login.tsx
index 068b1ca8..2141d9e9 100644
--- a/src/app/pages/login.tsx
+++ b/src/app/pages/login.tsx
@@ -1,9 +1,26 @@
+import { Windows } from '@/app/components/controls/windows'
+import { AppTitle } from '@/app/components/header/app-title'
import { LoginForm } from '@/app/components/login/form'
+import { isLinux, isWindows } from '@/utils/osType'
+import { tauriDragRegion } from '@/utils/tauriDragRegion'
export default function Login() {
return (
-
-
+
+ {!isLinux && (
+
+
+
+ {isWindows && }
+
+
+ )}
+
+
+
)
}
diff --git a/src/app/tables/playlists-columns.tsx b/src/app/tables/playlists-columns.tsx
index 1c2bd165..20424fbf 100644
--- a/src/app/tables/playlists-columns.tsx
+++ b/src/app/tables/playlists-columns.tsx
@@ -96,8 +96,8 @@ export function playlistsColumns(): ColumnDefType
[] {
enableSorting: true,
sortingFn: 'basic',
style: {
- width: 140,
- maxWidth: 140,
+ width: 190,
+ maxWidth: 190,
},
header: ({ column, table }) => (
diff --git a/src/i18n/index.ts b/src/i18n/index.ts
index 4794dc27..cdebfc76 100644
--- a/src/i18n/index.ts
+++ b/src/i18n/index.ts
@@ -7,7 +7,7 @@ i18n
.use(LanguageDetector)
.use(initReactI18next)
.init({
- debug: true,
+ debug: process.env.NODE_ENV === 'development',
fallbackLng: 'en',
interpolation: {
escapeValue: false,
diff --git a/src/i18n/languages.ts b/src/i18n/languages.ts
index 8a7d4be6..6a0ef803 100644
--- a/src/i18n/languages.ts
+++ b/src/i18n/languages.ts
@@ -1,8 +1,10 @@
import en from './locales/en.json'
+import es from './locales/es.json'
import ptBr from './locales/pt-BR.json'
export const resources = {
'en-US': { translation: en },
+ 'es-ES': { translation: es },
'pt-BR': { translation: ptBr },
}
@@ -13,6 +15,12 @@ export const languages = [
flag: 'US',
dayjsLocale: 'en',
},
+ {
+ nativeName: 'Español (España)',
+ langCode: 'es-ES',
+ flag: 'ES',
+ dayjsLocale: 'es',
+ },
{
nativeName: 'Português (Brasil)',
langCode: 'pt-BR',
diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json
index d1068ba7..b6fd5a42 100644
--- a/src/i18n/locales/en.json
+++ b/src/i18n/locales/en.json
@@ -20,13 +20,15 @@
"menu": {
"language": "Language",
"server": "Server",
+ "about": "About",
"serverLogout": "Logout"
},
"generic": {
"seeMore": "See more",
"showDetails": "Show details",
"hideDetails": "Hide details",
- "viewAll": "View all"
+ "viewAll": "View all",
+ "loading": "Loading..."
},
"theme": {
"label": "Theme",
@@ -423,6 +425,28 @@
"warnings": {
"reload": "There is a song playing!\nAre you sure you want to reload the page?"
},
+ "update": {
+ "dialog": {
+ "title": "New Version Available!",
+ "remindLater": "Remind Me Later",
+ "install": "Install and Restart"
+ },
+ "toasts": {
+ "started": "The update process has started.",
+ "success": "The update has been installed! The app will now restart.",
+ "error": "Failed to install the update. Please try downloading it directly from GitHub."
+ }
+ },
+ "about": {
+ "client": "Client",
+ "server": "Server",
+ "version": "Version:",
+ "type": "Type:",
+ "apiVersion": "API Version:",
+ "toasts": {
+ "copy": "Link copied to clipboard!"
+ }
+ },
"dayjs": {
"relativeTime": {
"future": "in %s",
diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json
new file mode 100644
index 00000000..5fd393a0
--- /dev/null
+++ b/src/i18n/locales/es.json
@@ -0,0 +1,472 @@
+{
+ "home": {
+ "recentlyPlayed": "Reproducido recientemente",
+ "mostPlayed": "Más reproducidos",
+ "explore": "Explorar",
+ "recentlyAdded": "Añadido recientemente"
+ },
+ "sidebar": {
+ "home": "Inicio",
+ "search": "Buscar...",
+ "miniSearch": "Buscar",
+ "library": "Biblioteca",
+ "artists": "Artistas",
+ "songs": "Canciones",
+ "albums": "Álbumes",
+ "playlists": "Listas de reproducción",
+ "radios": "Radios",
+ "emptyPlaylist": "Aún no se han creado listas de reproducción"
+ },
+ "menu": {
+ "language": "Idioma",
+ "server": "Servidor",
+ "about": "Acerca de",
+ "serverLogout": "Cerrar sesión"
+ },
+ "generic": {
+ "seeMore": "Ver más",
+ "showDetails": "Mostrar detalles",
+ "hideDetails": "Ocultar detalles",
+ "viewAll": "Ver todos",
+ "loading": "Cargando..."
+ },
+ "theme": {
+ "label": "Tema",
+ "light": "Claro",
+ "dark": "Oscuro",
+ "system": "Predeterminado del sistema"
+ },
+ "playlist": {
+ "headline": "Lista de reproducción",
+ "songCount_one": "{{count}} canción",
+ "songCount_many": "{{count}} canciones",
+ "songCount_other": "{{count}} canciones",
+ "refresh": "Actualizar listas de reproducción",
+ "buttons": {
+ "play": "Reproducir {{name}}",
+ "shuffle": "Reproducir {{name}} en modo aleatorio",
+ "options": "Más opciones para {{name}}"
+ },
+ "noSongList": "¡Esta lista de reproducción todavía no tiene canciones!",
+ "form": {
+ "labels": {
+ "name": "Nombre",
+ "commentDescription": "Escriba una breve descripción para esta lista de reproducción",
+ "isPublic": "Pública",
+ "comment": "Comentario",
+ "isPublicDescription": "Seleccione esta opción para hacer la lista de reproducción pública. Esto permitirá que otros usuarios puedan ver y acceder a su lista de reproducción."
+ },
+ "create": {
+ "title": "Crear lista de reproducción",
+ "button": "Crear",
+ "toast": {
+ "success": "¡Lista de reproducción creada con éxito!",
+ "error": "¡Error al crear lista de reproducción!"
+ }
+ },
+ "edit": {
+ "title": "Editar lista de reproducción",
+ "button": "Actualizar",
+ "toast": {
+ "success": "¡Lista de reproducción actualizada con éxito!",
+ "error": "¡Error al actualizar la lista de reproducción!"
+ }
+ },
+ "delete": {
+ "description": "Esta acción no se puede deshacer.",
+ "toast": {
+ "error": "¡Error al eliminar la lista de reproducción!",
+ "success": "¡Lista de reproducción eliminada con éxito!"
+ },
+ "title": "¿Seguro que quiere eliminar esta lista de reproducción?"
+ },
+ "validations": {
+ "nameLength": "La lista de reproducción debe tener al menos 2 caracteres."
+ },
+ "removeSong": {
+ "title_one": "¿Seguro que quiere eliminar la canción seleccionada?",
+ "title_many": "¿Seguro que quiere eliminar las canciones seleccionadas?",
+ "title_other": "¿Seguro que quiere eliminar las canciones seleccionadas?",
+ "description": "Esta acción no se puede deshacer.",
+ "toast": {
+ "success_one": "¡Canción eliminada con éxito!",
+ "success_many": "¡Canciones eliminadas con éxito!",
+ "success_other": "¡Canciones eliminadas con éxito!",
+ "error_one": "¡Error al eliminar la canción!",
+ "error_many": "¡Error al eliminar las canciones!",
+ "error_other": "¡Error al eliminar las canciones!"
+ }
+ }
+ },
+ "duration": "Cerca de {{duration}}"
+ },
+ "songs": {
+ "list": {
+ "search": {
+ "placeholder": "Buscar canciones..."
+ },
+ "filter": {
+ "clear": "Borrar filtro"
+ },
+ "byArtist": "Canciones de {{artist}}"
+ }
+ },
+ "album": {
+ "headline": "Álbum",
+ "buttons": {
+ "like": "Me gusta {{name}}",
+ "dislike": "Eliminar me gusta de {{name}}"
+ },
+ "info": {
+ "about": "Sobre {{name}}",
+ "lastfm": "Abrir en Last.fm",
+ "musicbrainz": "Abrir en MusicBrainz"
+ },
+ "more": {
+ "listTitle": "Más sobre este artista",
+ "discography": "Discografía del artista",
+ "genreTitle": "Más sobre {{genre}}"
+ },
+ "list": {
+ "header": {
+ "albumsByArtist": "Álbumes de {{artist}}"
+ },
+ "empty": {
+ "title": "¡Vaya, no hay álbumes aquí!",
+ "info": "Parece que no hay álbumes con el filtro actual.",
+ "action": "¿Intente ajustar los filtros?"
+ },
+ "genre": {
+ "search": "Encontrar un género...",
+ "loading": "Cargando géneros...",
+ "notFound": "No se ha encontrado el género.",
+ "label": "Seleccionar un género..."
+ },
+ "filter": {
+ "artist": "Artista",
+ "genre": "Género",
+ "favorites": "Favoritos",
+ "mostPlayed": "Más reproducidos",
+ "name": "Nombre",
+ "random": "Aleatorio",
+ "recentlyAdded": "Añadido recientemente",
+ "recentlyPlayed": "Reproducido recientemente",
+ "releaseYear": "Año de lanzamiento",
+ "discography": "Discografía",
+ "search": "Buscar"
+ },
+ "search": {
+ "placeholder": "Buscar álbumes..."
+ }
+ },
+ "table": {
+ "discNumber": "Disco {{number}}"
+ }
+ },
+ "artist": {
+ "headline": "Artista",
+ "buttons": {
+ "options": "Más opciones para {{artist}}",
+ "shuffle": "Reproducir radio de {{artist}} en modo aleatorio",
+ "play": "Reproducir radio de {{artist}}"
+ },
+ "info": {
+ "albumsCount_one": "{{count}} álbum",
+ "albumsCount_many": "{{count}} álbumes",
+ "albumsCount_other": "{{count}} álbumes"
+ },
+ "topSongs": "Popular",
+ "recentAlbums": "Álbumes recientes",
+ "relatedArtists": "Artistas relacionados"
+ },
+ "table": {
+ "columns": {
+ "title": "Título",
+ "artist": "Artista",
+ "album": "Álbum",
+ "year": "Año",
+ "duration": "Duración",
+ "plays": "Reproducciones",
+ "lastPlayed": "Última vez reproducido",
+ "bpm": "PPM",
+ "bitrate": "Tasa de bits",
+ "quality": "Calidad",
+ "name": "Nombre",
+ "public": "Público",
+ "size": "Tamaño",
+ "codec": "Códec",
+ "path": "Ruta",
+ "favorite": "Favorito",
+ "discNumber": "Número de disco",
+ "trackGain": "Ganancia de pista",
+ "trackPeak": "Pico de pista",
+ "albumPeak": "Pico de álbum",
+ "comment": "Comentario",
+ "track": "Pista",
+ "genres": "Géneros",
+ "albumCount": "Número de álbumes",
+ "songCount": "Número de canciones"
+ },
+ "buttons": {
+ "play": "Reproducir {{title}} de {{artist}}",
+ "pause": "Pausar {{title}} de {{artist}}"
+ },
+ "lastPlayed": "Hace {{date}}",
+ "pagination": {
+ "screenReader": {
+ "firstPage": "Ir a la primera página",
+ "previousPage": "Ir a la página anterior",
+ "nextPage": "Ir a la siguiente página",
+ "lastPage": "Ir a la última página"
+ },
+ "rowsPerPage": "Filas por página",
+ "currentPage": "Página {{currentPage}} de {{totalPages}}"
+ },
+ "sort": {
+ "desc": "Descendente",
+ "reset": "Restablecer",
+ "asc": "Ascendente"
+ },
+ "menu": {
+ "selectedCount_one": "{{count}} seleccionado",
+ "selectedCount_many": "{{count}} seleccionados",
+ "selectedCount_other": "{{count}} seleccionados"
+ }
+ },
+ "fullscreen": {
+ "playing": "En reproducción",
+ "noLyrics": "No se ha encontrado letra",
+ "lyrics": "Letra",
+ "switchButton": "Cambiar a pantalla completa",
+ "queue": "Cola",
+ "loadingLyrics": "Buscando letra..."
+ },
+ "logout": {
+ "dialog": {
+ "cancel": "Cancelar",
+ "confirm": "Continuar",
+ "description": "Confirmar para cerrar sesión.",
+ "title": "¿Preparado para despedirse por ahora?"
+ }
+ },
+ "login": {
+ "form": {
+ "server": "Servidor",
+ "description": "Conectar a su servidor de Subsonic.",
+ "username": "Nombre de usuario",
+ "connect": "Conectar",
+ "connecting": "Conectando...",
+ "validations": {
+ "url": "Por favor introduce una dirección URL válida.",
+ "protocol": "La dirección URL debe comenzar con http:// o https://",
+ "username": "Por favor, proporcione un nombre de usuario",
+ "passwordLength": "La contraseña debe tener al menos 2 caracteres.",
+ "password": "Por favor, proporcione una contraseña",
+ "usernameLength": "El nombre de usuario debe tener al menos 2 caracteres."
+ },
+ "urlDescription": "Esta es su dirección URL",
+ "url": "Dirección URL",
+ "password": "Contraseña",
+ "usernamePlaceholder": "Su nombre de usuario"
+ }
+ },
+ "toast": {
+ "server": {
+ "success": "¡El servidor se ha guardado con éxito!",
+ "error": "¡Error de comunicación con el servidor!"
+ }
+ },
+ "command": {
+ "inputPlaceholder": "Buscar un álbum, artista o canción",
+ "noResults": "No se han encontrado resultados.",
+ "commands": {
+ "theme": "Cambiar tema",
+ "heading": "Comandos",
+ "pages": "Ir a página"
+ },
+ "pages": "Páginas"
+ },
+ "player": {
+ "noSongPlaying": "No se reproduce ninguna canción",
+ "tooltips": {
+ "shuffle": {
+ "enable": "Activar aleatorio",
+ "disable": "Desactivar aleatorio"
+ },
+ "previous": "Anterior",
+ "play": "Reproducir",
+ "next": "Siguiente",
+ "repeat": {
+ "enable": "Activar repetir",
+ "disable": "Desactivar repetir",
+ "enableOne": "Activar repetir una vez"
+ },
+ "like": "Me gusta {{song}} de {{artist}}",
+ "dislike": "Eliminar me gusta de {{song}} de {{artist}}",
+ "pause": "Pausa"
+ },
+ "noRadioPlaying": "No se reproduce ninguna radio"
+ },
+ "options": {
+ "play": "Reproducir",
+ "addLast": "Añadir al final",
+ "download": "Descargar",
+ "playlist": {
+ "add": "Añadir a lista de reproducción",
+ "edit": "Editar lista de reproducción",
+ "delete": "Eliminar lista de reproducción",
+ "search": "Encontrar una lista de reproducción",
+ "create": "Nueva lista de reproducción",
+ "notFound": "No se ha encontrado la lista de reproducción",
+ "removeSong": "Eliminar de la lista de reproducción"
+ },
+ "info": "Obtener información",
+ "playNext": "Reproducir siguiente"
+ },
+ "radios": {
+ "label": "Radio",
+ "addRadio": "Añadir radio",
+ "table": {
+ "name": "Nombre",
+ "stream": "Dirección URL de retransmisión",
+ "actions": {
+ "edit": "Editar radio",
+ "delete": "Eliminar radio"
+ },
+ "playTooltip": "Reproducir radio {{name}}",
+ "homepage": "Dirección URL de la página de inicio",
+ "pauseTooltip": "Pausar radio {{name}}"
+ },
+ "empty": {
+ "info": "Haga clic para añadir su primera emisora de radio.",
+ "title": "¡No tienes ninguna radio!"
+ },
+ "form": {
+ "create": {
+ "title": "Añadir radio",
+ "button": "Guardar",
+ "toast": {
+ "success": "¡Radio añadida con éxito!",
+ "error": "¡Error al crear una radio!"
+ }
+ },
+ "edit": {
+ "button": "Actualizar",
+ "title": "Editar radio",
+ "toast": {
+ "success": "¡Radio actualizada con éxito!",
+ "error": "¡Error al actualizar la radio!"
+ }
+ },
+ "delete": {
+ "title": "¿Seguro que quiere eliminar esta emisora de radio?",
+ "description": "Esta acción no se puede deshacer.",
+ "toast": {
+ "error": "¡Errro al eliminar la radio!",
+ "success": "¡Radio eliminada con éxito!"
+ }
+ },
+ "validations": {
+ "name": "El nombre de la radio debe tener al menos 3 caracteres.",
+ "homepageUrlLength": "La dirección URL de la página de inicio debe tener al menos 10 caracteres",
+ "streamUrlLength": "La dirección URL de retransmisión debe tener al menos 10 caracteres",
+ "url": "Por favor, proporcione una dirección URL válida."
+ }
+ }
+ },
+ "time": {
+ "days_one": "{{days}} día",
+ "days_many": "{{days}} días",
+ "days_other": "{{days}} días",
+ "hour": "{{hour}} h",
+ "minutes": "{{minutes}} min",
+ "seconds": "{{seconds}} s"
+ },
+ "server": {
+ "songCount_one": "{{count}} canción",
+ "songCount_many": "{{count}} canciones",
+ "songCount_other": "{{count}} canciones",
+ "folderCount_one": "{{count}} carpeta",
+ "folderCount_many": "{{count}} carpetas",
+ "folderCount_other": "{{count}} carpetas",
+ "lastScan": "Última exploración: {{date}}",
+ "status": "Estado",
+ "buttons": {
+ "refresh": "Actualizar estado",
+ "startScan": "Exploración rápida"
+ },
+ "management": "Gestión de servidor"
+ },
+ "downloads": {
+ "started": "Descarga iniciada.",
+ "failed": "Hubo un error al descargar su archivo.",
+ "completed": "Se ha descargado su archivo."
+ },
+ "queue": {
+ "clear": "Limpiar cola",
+ "title": "Cola"
+ },
+ "shortcuts": {
+ "modal": {
+ "title": "Atajos de teclado",
+ "description": {
+ "first": "Presionar",
+ "last": "para activar esta ventana modal."
+ }
+ },
+ "playback": {
+ "label": "Reproducción",
+ "repeat": "Repetir",
+ "previous": "Saltar a la anterior",
+ "next": "Saltar a la siguiente",
+ "play": "Reproducir / Pausar",
+ "shuffle": "Aleatorio"
+ }
+ },
+ "songInfo": {
+ "title": "Detalles de la canción",
+ "error": "No se pudo recuperar la información de la canción. Por favor, inténtelo de nuevo más tarde."
+ },
+ "warnings": {
+ "reload": "¡Hay una canción en reproducción!\n¿Seguro que quiere recargar la página?"
+ },
+ "update": {
+ "dialog": {
+ "title": "¡Nueva Versión Disponible!",
+ "remindLater": "Recordarme Más Tarde",
+ "install": "Instalar y Reiniciar"
+ },
+ "toasts": {
+ "started": "El proceso de actualización ha comenzado.",
+ "success": "¡La actualización se ha instalado! La aplicación se reiniciará ahora.",
+ "error": "Error al instalar la actualización. Por favor, intente descargarla directamente desde GitHub."
+ }
+ },
+ "about": {
+ "client": "Cliente",
+ "server": "Servidor",
+ "version": "Versión:",
+ "type": "Tipo:",
+ "apiVersion": "Versión de la API:",
+ "toasts": {
+ "copy": "¡Enlace copiado al portapapeles!"
+ }
+ },
+ "dayjs": {
+ "relativeTime": {
+ "future": "en %s",
+ "past": "Hace %s",
+ "s": "unos segundos",
+ "m": "1 minuto",
+ "mm": "%d minutos",
+ "M": "1 mes",
+ "MM": "%d meses",
+ "d": "1 día",
+ "dd": "%d días",
+ "y": "1 año",
+ "yy": "%d años",
+ "hh": "%d horas",
+ "h": "1 hora"
+ }
+ }
+}
diff --git a/src/i18n/locales/pt-BR.json b/src/i18n/locales/pt-BR.json
index 11cf46a2..2477e425 100644
--- a/src/i18n/locales/pt-BR.json
+++ b/src/i18n/locales/pt-BR.json
@@ -20,13 +20,15 @@
"menu": {
"language": "Linguagem",
"server": "Servidor",
+ "about": "Sobre",
"serverLogout": "Desconectar"
},
"generic": {
"seeMore": "Ver mais",
"showDetails": "Exibir detalhes",
"hideDetails": "Ocultar detalhes",
- "viewAll": "Ver tudo"
+ "viewAll": "Ver tudo",
+ "loading": "Carregando..."
},
"theme": {
"label": "Tema",
@@ -423,6 +425,28 @@
"warnings": {
"reload": "Há uma música tocando!\nVocê tem certeza que deseja recarregar a página?"
},
+ "update": {
+ "dialog": {
+ "title": "Nova Versão Disponível!",
+ "remindLater": "Lembrar-me Mais Tarde",
+ "install": "Instalar e Reiniciar"
+ },
+ "toasts": {
+ "started": "O processo de atualização foi iniciado.",
+ "success": "A atualização foi instalada! O aplicativo será reiniciado agora.",
+ "error": "Falha ao instalar a atualização. Por favor, tente baixá-la diretamente do GitHub."
+ }
+ },
+ "about": {
+ "client": "Cliente",
+ "server": "Servidor",
+ "version": "Versão:",
+ "type": "Tipo:",
+ "apiVersion": "Versão da API:",
+ "toasts": {
+ "copy": "Link copiado para a área de transferência!"
+ }
+ },
"dayjs": {
"relativeTime": {
"future": "em %s",
diff --git a/src/index.css b/src/index.css
index 26240389..a8bb6512 100644
--- a/src/index.css
+++ b/src/index.css
@@ -57,7 +57,13 @@
@apply border-border;
}
body {
- @apply bg-background text-foreground subpixel-antialiased;
+ @apply bg-background text-foreground;
+ }
+ body.windows-linux {
+ @apply antialiased;
+ }
+ body.mac {
+ @apply subpixel-antialiased;
}
:root {
--header-height: 44px;
@@ -113,7 +119,6 @@ textarea {
}
img {
- user-drag: none;
-webkit-user-drag: none;
user-select: none;
-moz-user-select: none;
@@ -210,4 +215,16 @@ img {
.user-dropdown-trigger[data-state="open"] > :first-child {
@apply bg-accent;
+}
+
+#update-info-body ul {
+ @apply list-disc list-inside;
+}
+
+#update-info-body h1,
+#update-info-body h2,
+#update-info-body h3,
+#update-info-body h4,
+#update-info-body h5 {
+ @apply font-semibold text-foreground;
}
\ No newline at end of file
diff --git a/src/main.tsx b/src/main.tsx
index cccaba82..ca1d0279 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -12,15 +12,14 @@ import '@/i18n'
import App from '@/App'
import { queryClient } from '@/lib/queryClient'
-import {
- preventContextMenu,
- preventNewTabAndScroll,
- preventReload,
-} from '@/utils/browser'
+import { blockFeatures } from '@/utils/browser'
+import { isLinux } from '@/utils/osType'
-preventContextMenu()
-preventNewTabAndScroll()
-preventReload()
+if (isLinux) {
+ import('@/tw-fix-linux.css')
+}
+
+blockFeatures()
createRoot(document.getElementById('root') as HTMLElement).render(
diff --git a/src/service/ping.ts b/src/service/ping.ts
index 6f53c1ad..38df124f 100644
--- a/src/service/ping.ts
+++ b/src/service/ping.ts
@@ -1,17 +1,19 @@
import { httpClient } from '@/api/httpClient'
import { SubsonicResponse } from '@/types/responses/subsonicResponse'
+async function pingInfo() {
+ const response = await httpClient('/ping.view', {
+ method: 'GET',
+ })
+
+ return response?.data
+}
+
async function pingView() {
try {
- const response = await httpClient('/ping.view', {
- method: 'GET',
- })
+ const info = await pingInfo()
- if (response?.data.status === 'ok') {
- return true
- } else {
- return false
- }
+ return info?.status === 'ok'
} catch (error) {
console.error(error)
return false
@@ -19,5 +21,6 @@ async function pingView() {
}
export const ping = {
+ pingInfo,
pingView,
}
diff --git a/src/store/app.store.ts b/src/store/app.store.ts
index 59495a2a..18537c30 100644
--- a/src/store/app.store.ts
+++ b/src/store/app.store.ts
@@ -37,6 +37,20 @@ export const useAppStore = createWithEqualityFn()(
})
},
},
+ update: {
+ openDialog: false,
+ setOpenDialog: (value) => {
+ set((state) => {
+ state.update.openDialog = value
+ })
+ },
+ remindOnNextBoot: false,
+ setRemindOnNextBoot: (value) => {
+ set((state) => {
+ state.update.remindOnNextBoot = value
+ })
+ },
+ },
actions: {
setOsType: (value) => {
set((state) => {
@@ -129,6 +143,8 @@ export const useAppStore = createWithEqualityFn()(
state,
'data.logoutDialogState',
'data.hideServer',
+ 'command.open',
+ 'update',
)
return appStore
@@ -141,3 +157,4 @@ export const useAppStore = createWithEqualityFn()(
export const useAppData = () => useAppStore((state) => state.data)
export const useAppDataPages = () => useAppStore((state) => state.data.pages)
export const useAppActions = () => useAppStore((state) => state.actions)
+export const useAppUpdate = () => useAppStore((state) => state.update)
diff --git a/src/tw-fix-linux.css b/src/tw-fix-linux.css
new file mode 100644
index 00000000..1827252a
--- /dev/null
+++ b/src/tw-fix-linux.css
@@ -0,0 +1,28 @@
+.backdrop-blur-sm {
+ -webkit-backdrop-filter: blur(4px) !important;
+ backdrop-filter: blur(4px) !important;
+}
+.backdrop-blur {
+ -webkit-backdrop-filter: blur(8px) !important;
+ backdrop-filter: blur(8px) !important;
+}
+.backdrop-blur-md {
+ -webkit-backdrop-filter: blur(12px) !important;
+ backdrop-filter: blur(12px) !important;
+}
+.backdrop-blur-lg {
+ -webkit-backdrop-filter: blur(16px) !important;
+ backdrop-filter: blur(16px) !important;
+}
+.backdrop-blur-xl {
+ -webkit-backdrop-filter: blur(24px) !important;
+ backdrop-filter: blur(24px) !important;
+}
+.backdrop-blur-2xl {
+ -webkit-backdrop-filter: blur(40px) !important;
+ backdrop-filter: blur(40px) !important;
+}
+.backdrop-blur-3xl {
+ -webkit-backdrop-filter: blur(64px) !important;
+ backdrop-filter: blur(64px) !important;
+}
\ No newline at end of file
diff --git a/src/types/serverConfig.ts b/src/types/serverConfig.ts
index e7ca0dfd..588da91f 100644
--- a/src/types/serverConfig.ts
+++ b/src/types/serverConfig.ts
@@ -39,8 +39,16 @@ export interface IAppCommand {
setOpen: (value: boolean) => void
}
+export interface IAppUpdate {
+ openDialog: boolean
+ setOpenDialog: (value: boolean) => void
+ remindOnNextBoot: boolean
+ setRemindOnNextBoot: (value: boolean) => void
+}
+
export interface IAppContext {
data: IAppData
command: IAppCommand
actions: IAppActions
+ update: IAppUpdate
}
diff --git a/src/utils/appName.ts b/src/utils/appName.ts
index d0949fc8..27fe0a50 100644
--- a/src/utils/appName.ts
+++ b/src/utils/appName.ts
@@ -1 +1,11 @@
+import { version, repository } from '@/../package.json'
+
export const appName = 'Aonsoku'
+
+export function getAppInfo() {
+ return {
+ name: appName,
+ version,
+ url: repository.url,
+ }
+}
diff --git a/src/utils/browser.ts b/src/utils/browser.ts
index f197763a..3e96c0ae 100644
--- a/src/utils/browser.ts
+++ b/src/utils/browser.ts
@@ -1,5 +1,6 @@
import i18n from '@/i18n'
import { usePlayerStore } from '@/store/player.store'
+import { isMac } from './osType'
import { isTauri } from './tauriTools'
export enum MouseButton {
@@ -15,7 +16,7 @@ export function isMacOS() {
return isMac ?? false
}
-export function preventContextMenu() {
+function preventContextMenu() {
document.addEventListener('contextmenu', (e) => {
if (
e.target instanceof HTMLInputElement ||
@@ -33,7 +34,7 @@ function isAnyModifierKeyPressed(e: MouseEvent) {
return e.ctrlKey || e.metaKey || e.shiftKey || e.altKey
}
-export function preventNewTabAndScroll() {
+function preventNewTabAndScroll() {
// Prevent new tab on middle click
document.addEventListener('auxclick', (e) => {
e.preventDefault()
@@ -54,7 +55,7 @@ export function preventNewTabAndScroll() {
})
}
-export function preventReload() {
+function preventReload() {
document.addEventListener('keydown', (e) => {
const isF5 = e.key === 'F5'
const isReloadCmd = (e.ctrlKey || e.metaKey) && e.key === 'r'
@@ -77,3 +78,48 @@ export function preventReload() {
window.location.reload()
})
}
+
+function preventAltBehaviour() {
+ document.addEventListener('keydown', (e) => {
+ if (e.altKey) {
+ e.preventDefault()
+ }
+ })
+}
+
+export function enterFullscreen() {
+ const element = document.documentElement
+ if (element.requestFullscreen) {
+ element.requestFullscreen()
+ }
+ if ('webkitRequestFullscreen' in element) {
+ // @ts-expect-error no types for webkit
+ element.webkitRequestFullscreen()
+ }
+}
+
+export function exitFullscreen() {
+ if (document.exitFullscreen) {
+ document.exitFullscreen()
+ }
+ if ('webkitExitFullscreen' in document) {
+ // @ts-expect-error no types for webkit
+ document.webkitExitFullscreen()
+ }
+}
+
+function setFontSmoothing() {
+ if (isMacOS() || isMac) {
+ document.body.classList.add('mac')
+ } else {
+ document.body.classList.add('windows-linux')
+ }
+}
+
+export function blockFeatures() {
+ preventContextMenu()
+ preventNewTabAndScroll()
+ preventReload()
+ preventAltBehaviour()
+ setFontSmoothing()
+}
diff --git a/src/utils/dateTime.ts b/src/utils/dateTime.ts
index 9f4cb87d..d306f264 100644
--- a/src/utils/dateTime.ts
+++ b/src/utils/dateTime.ts
@@ -6,6 +6,7 @@ import timezone from 'dayjs/plugin/timezone'
import updateLocale from 'dayjs/plugin/updateLocale'
import utc from 'dayjs/plugin/utc'
import 'dayjs/locale/pt-br'
+import 'dayjs/locale/es'
import i18n from '@/i18n'
import { languages, resources } from '@/i18n/languages'
diff --git a/src/utils/osType.ts b/src/utils/osType.ts
new file mode 100644
index 00000000..a5811d9b
--- /dev/null
+++ b/src/utils/osType.ts
@@ -0,0 +1,10 @@
+import { type } from '@tauri-apps/plugin-os'
+import { isTauri } from './tauriTools'
+
+export function getOsType() {
+ return Promise.resolve(type())
+}
+
+export const isWindows = isTauri() ? type() === 'windows' : false
+export const isMac = isTauri() ? type() === 'macos' : false
+export const isLinux = isTauri() ? type() === 'linux' : false
diff --git a/src/utils/queryKeys.ts b/src/utils/queryKeys.ts
index 909ef2f2..b86c46f5 100644
--- a/src/utils/queryKeys.ts
+++ b/src/utils/queryKeys.ts
@@ -37,6 +37,11 @@ const search = 'search-key'
const genre = 'get-all-genres'
+const update = {
+ serverInfo: 'get-server-info',
+ check: 'check-for-updates',
+}
+
export const queryKeys = {
album,
artist,
@@ -45,4 +50,5 @@ export const queryKeys = {
radio,
search,
genre,
+ update,
}
diff --git a/src/utils/tauriDragRegion.ts b/src/utils/tauriDragRegion.ts
new file mode 100644
index 00000000..a8061fdf
--- /dev/null
+++ b/src/utils/tauriDragRegion.ts
@@ -0,0 +1,10 @@
+import { isLinux } from './osType'
+import { isTauri } from './tauriTools'
+
+function setDataTauriDragRegion() {
+ if (!isTauri() || isLinux) return {}
+
+ return { 'data-tauri-drag-region': true }
+}
+
+export const tauriDragRegion = setDataTauriDragRegion()
diff --git a/tsconfig.json b/tsconfig.json
index b1c77e02..71adc52f 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -36,7 +36,8 @@
"./src/types"
],
"types": [
- "cypress"
+ "cypress",
+ "node"
]
},
"include": [