diff --git a/materialious/android/app/build.gradle b/materialious/android/app/build.gradle index 17093e50..5fa7ddf5 100644 --- a/materialious/android/app/build.gradle +++ b/materialious/android/app/build.gradle @@ -7,8 +7,8 @@ android { applicationId "us.materialio.app" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 235 - versionName "1.16.18" + versionCode 236 + versionName "1.16.19" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. diff --git a/materialious/electron/materialious.metainfo.xml b/materialious/electron/materialious.metainfo.xml index 57331d56..d424d0cb 100644 --- a/materialious/electron/materialious.metainfo.xml +++ b/materialious/electron/materialious.metainfo.xml @@ -77,7 +77,11 @@ - + + + https://github.com/Materialious/Materialious/releases/tag/1.16.19 + + https://github.com/Materialious/Materialious/releases/tag/1.16.18 diff --git a/materialious/electron/package.json b/materialious/electron/package.json index b6f7dfad..14a64b1b 100644 --- a/materialious/electron/package.json +++ b/materialious/electron/package.json @@ -1,6 +1,6 @@ { "name": "Materialious", - "version": "1.16.18", + "version": "1.16.19", "description": "Modern material design for YouTube and Invidious.", "author": { "name": "Ward Pearce", diff --git a/materialious/package-lock.json b/materialious/package-lock.json index c781b08a..0e9d1908 100644 --- a/materialious/package-lock.json +++ b/materialious/package-lock.json @@ -9140,9 +9140,9 @@ } }, "node_modules/devalue": { - "version": "5.6.3", - "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.3.tgz", - "integrity": "sha512-nc7XjUU/2Lb+SvEFVGcWLiKkzfw8+qHI7zn8WYXKkLMgfGSHbgCEaR6bJpev8Cm6Rmrb19Gfd/tZvGqx9is3wg==", + "version": "5.6.4", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.4.tgz", + "integrity": "sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA==", "license": "MIT" }, "node_modules/dexie": { diff --git a/materialious/package.json b/materialious/package.json index 645b8370..3f404bd7 100644 --- a/materialious/package.json +++ b/materialious/package.json @@ -1,6 +1,6 @@ { "name": "materialious", - "version": "1.16.18", + "version": "1.16.19", "private": true, "scripts": { "dev": "npm run patch:github && vite dev", diff --git a/materialious/src/lib/api/backend/history.ts b/materialious/src/lib/api/backend/history.ts index 2bdf6566..af39ace3 100644 --- a/materialious/src/lib/api/backend/history.ts +++ b/materialious/src/lib/api/backend/history.ts @@ -1,7 +1,8 @@ import sodium from 'libsodium-wrappers-sumo'; import { decryptWithMasterKey, encryptWithMasterKey, getRawKey, getSecureHash } from './encryption'; -import type { VideoPlay, VideoWatchHistory } from '../model'; +import type { VideoWatchHistory } from '../model'; import { getBestThumbnail } from '$lib/images'; +import type { ThumbnailVideo } from '$lib/thumbnail'; export async function updateWatchHistoryBackend(videoId: string, progress: number) { await sodium.ready; @@ -128,7 +129,7 @@ export async function deleteWatchHistoryItemBackend(videoId: string) { await fetch(`/api/user/history/${videoHash}`, { method: 'DELETE' }); } -export async function saveWatchHistoryBackend(video: VideoPlay, progress: number = 0) { +export async function saveWatchHistoryBackend(video: ThumbnailVideo, progress: number = 0) { await sodium.ready; const rawKey = await getRawKey(); if (!rawKey) return; @@ -137,7 +138,9 @@ export async function saveWatchHistoryBackend(video: VideoPlay, progress: number const title = await encryptWithMasterKey(video.title); const author = await encryptWithMasterKey(video.author); - const thumbnail = await encryptWithMasterKey(getBestThumbnail(video.videoThumbnails)); + const thumbnail = await encryptWithMasterKey( + 'videoThumbnails' in video ? getBestThumbnail(video.videoThumbnails) : video.thumbnail + ); const videoId = await encryptWithMasterKey(video.videoId); await fetch('/api/user/history', { diff --git a/materialious/src/lib/api/historyPool.ts b/materialious/src/lib/api/historyPool.ts index e588f3dc..2a05c267 100644 --- a/materialious/src/lib/api/historyPool.ts +++ b/materialious/src/lib/api/historyPool.ts @@ -5,7 +5,7 @@ const videoIds: string[] = []; const pendingResolves = new Map void>(); let timeout: ReturnType | null = null; -const DEBOUNCE_MS = 2000; +const DEBOUNCE_MS = 1000; const BATCH_SIZE = 100; async function processBatches(): Promise { diff --git a/materialious/src/lib/api/index.ts b/materialious/src/lib/api/index.ts index 6039278c..5fbb11fa 100644 --- a/materialious/src/lib/api/index.ts +++ b/materialious/src/lib/api/index.ts @@ -82,6 +82,7 @@ import { } from './backend/history'; import { localDb } from '$lib/dexie'; import { getBestThumbnail } from '$lib/images'; +import type { ThumbnailVideo } from '$lib/thumbnail'; export async function getPopular(fetchOptions?: RequestInit): Promise { // Doesn't exist in YTjs. @@ -368,7 +369,7 @@ export async function updateWatchHistory( await localDb.watchHistory.update({ videoId }, { progress, watched: new Date() }); } -export async function saveWatchHistory(video: VideoPlay, progress: number = 0) { +export async function saveWatchHistory(video: ThumbnailVideo, progress: number = 0) { if (!get(watchHistoryEnabledStore)) return; if (isOwnBackend()?.internalAuth && get(rawMasterKeyStore)) { @@ -384,7 +385,8 @@ export async function saveWatchHistory(video: VideoPlay, progress: number = 0) { progress, id: video.videoId, title: video.title, - thumbnail: getBestThumbnail(video.videoThumbnails), + thumbnail: + 'videoThumbnails' in video ? getBestThumbnail(video.videoThumbnails) : video.thumbnail, videoId: video.videoId, type: 'historyVideo' }); diff --git a/materialious/src/lib/api/invidious/request.ts b/materialious/src/lib/api/invidious/request.ts index fd4ba9ae..bdf2fa9f 100644 --- a/materialious/src/lib/api/invidious/request.ts +++ b/materialious/src/lib/api/invidious/request.ts @@ -8,13 +8,11 @@ export function buildPath(path: string): URL { export function setLocale(url: URL): URL { const region = get(interfaceRegionStore); - if (region) { url.searchParams.set('region', region); } const locale = getUserLocale(); - url.searchParams.set('hl', locale); return url; diff --git a/materialious/src/lib/components/ComboBox.svelte b/materialious/src/lib/components/ComboBox.svelte new file mode 100644 index 00000000..bb55b58a --- /dev/null +++ b/materialious/src/lib/components/ComboBox.svelte @@ -0,0 +1,79 @@ + + +
+ + {#if label} + + {/if} + keyboard_arrow_down + +
+ {#each filtered as option (option)} +
+ {option.label} + {#if combobox.isSelected(option.value)} + check + {/if} +
+ {:else} + {$_('noResult')} + {/each} +
+
diff --git a/materialious/src/lib/components/Share.svelte b/materialious/src/lib/components/Share.svelte index b88b4d0e..4b53afe1 100644 --- a/materialious/src/lib/components/Share.svelte +++ b/materialious/src/lib/components/Share.svelte @@ -17,11 +17,17 @@ let { shares, includePromptText = undefined, - iconOnly = true + iconOnly = true, + classes = 'surface-container-highest', + menuClasses = 'mobile no-wrap', + onShare = undefined }: { shares: ShareLink[]; includePromptText?: string; iconOnly: boolean; + classes?: string; + menuClasses?: string; + onShare?: (value: string) => void; } = $props(); const shareBase = { @@ -34,19 +40,21 @@ let shareButtonElement: HTMLElement | undefined = $state(); let includePrompt = $state(false); - async function onShare(share: ShareLink) { + async function onShareValue(share: ShareLink) { const url = new URL(`${shareBase[share.type]}${share.path}`); if (share.param && includePrompt) url.searchParams.append(share.param.key, share.param.value().toString()); + onShare?.(url.toString()); + await shareURL(url.toString()); shareButtonElement?.click(); } - + +
  • + +
  • + + {/if}
    @@ -138,7 +271,7 @@ style="height: 200px;" > - {#if progress !== undefined} + {#if progress}
    check
    @@ -184,41 +317,53 @@
    @@ -291,6 +436,7 @@ word-wrap: break-word; overflow: hidden; border-radius: 0px; + width: 100%; } .thumbnail-details { @@ -320,6 +466,10 @@ align-items: end; } + .root-menu > li:hover { + background-color: transparent; + } + @media screen and (max-width: 1800px) { .sideways-root .thumbnail { width: 100%; diff --git a/materialious/src/lib/css/global.css b/materialious/src/lib/css/global.css index 0942f297..08a08186 100644 --- a/materialious/src/lib/css/global.css +++ b/materialious/src/lib/css/global.css @@ -194,6 +194,24 @@ menu.player-settings { transform: translate(-50%, -50%); } +[data-melt-combobox-content] { + background-color: var(--surface-container-high); + padding: 1rem; + font-size: 1rem; + border-radius: var(--border-radius); + overflow-y: scroll; + max-height: 200px; +} + +[data-melt-combobox-option] { + padding: 0.25rem; + cursor: pointer; +} + +[data-highlighted] { + background-color: var(--surface-container-highest); +} + @media screen and (max-width: 1000px) { menu.mobile { position: fixed !important; diff --git a/materialious/src/lib/i18n/locales/en.json b/materialious/src/lib/i18n/locales/en.json index 6df66699..686dfe62 100644 --- a/materialious/src/lib/i18n/locales/en.json +++ b/materialious/src/lib/i18n/locales/en.json @@ -38,6 +38,10 @@ "sortBy": "Sort By", "features": "Features" }, + "markAs": { + "watched": "Mark as watched", + "unwatched": "Mark as unwatched" + }, "initalSetup": { "useInvidious": "I want to use Invidious", "useLocalFallback": "I want to use local video fallback", @@ -73,6 +77,7 @@ "letterCase": "Letter case for titles", "hideReplies": "Hide replies", "hideVideo": "Hide video", + "hideAuthor": "Hide author", "defaultPage": "Default page", "newest": "Newest", "oldest": "Oldest", @@ -269,7 +274,9 @@ "optionPlaceholder": "Select your option", "value": "Value", "addConditional": "Add conditional", - "addFilter": "Add filter" + "addFilter": "Add filter", + "videoFiltered": "\"{{videoTitle}}\" from \"{{authorName}}\" filtered", + "channelFiltered": "\"{{authorName}}\" filtered" }, "export": { "title": "Export/Import", diff --git a/materialious/src/lib/i18n/locales/es.json b/materialious/src/lib/i18n/locales/es.json index 874303e2..d4038da8 100644 --- a/materialious/src/lib/i18n/locales/es.json +++ b/materialious/src/lib/i18n/locales/es.json @@ -12,15 +12,15 @@ "delete": "Eliminar", "donate": "Donar", "deleteAllHistory": "Eliminar todo el historial", - "skipping": "Omitir", + "skipping": "Omitiendo", "replies": "respuestas", "region": "Región", "noResult": "Sin resultados", - "language": "Idioma", + "language": "Lenguaje", "channels": "canales", "transcript": "Transcribir", "noCaptionData": "Los subtítulos no contienen datos.", - "selectLang": "Seleccionar idioma", + "selectLang": "Seleccionar lenguaje", "letterCase": "Títulos en mayúsculas o minúsculas", "hideReplies": "Ocultar respuestas", "hideVideo": "Ocultar video", @@ -48,7 +48,9 @@ "private": "Privado", "playVideos": "Reproducir videos", "shuffleVideos": "Reproducir videos en aleatorio", - "loopPlaylist": "Repetir lista de reproducción" + "loopPlaylist": "Repetir lista de reproducción", + "next": "Siguiente", + "previous": "Anterior" }, "thumbnail": { "live": "EN VIVO", @@ -73,7 +75,10 @@ "title": "Compartir", "materialiousLink": "Copiar enlace de Materialious", "invidiousRedirect": "Copiar enlace de redirección de Invidious", - "youtubeLink": "Copiar enlace de Youtube" + "youtubeLink": "Copiar enlace de Youtube", + "copyXLink": "Copiar {{linkType}} link", + "includeTimestamp": "Marca de tiempo", + "copiedSuccess": "Copiado al portapapeles" }, "download": "Descargar", "downloadSteps": { @@ -89,8 +94,25 @@ "noPlaylists": "Sin listas de reproducción", "unableToLoadComments": "No se pudo cargar los comentarios", "addToPlaylist": "Agregar a lista de reproducción", - "youtubeJsFallBack": "Se está utilizando la opción de video local.", - "defaultLanguage": "Idioma predeterminado" + "youtubeJsFallBack": "Se está utilizando la opción de video local", + "defaultLanguage": "Idioma predeterminado", + "preferredQuality": "Calidad preferida", + "premiere": "El video estará disponible", + "chapters": "Capítulos", + "youtubeJsLoading": "El video no ha podido cargar, se está intentado usar la opción de video local.", + "retryText": "El video no ha podido cargar, ¿cómo le gustaría proceder?", + "enableYoutubejsTemp": "Usar la opción de video local una vez", + "enableYoutubejsPerm": "Siempre permitir la opción de video local", + "controls": { + "back": "Atrás", + "quality": "Calidad", + "auto": "Automático", + "playbackSpeed": "Velocidad de reproducción", + "loop": "Repetir", + "on": "Encendido", + "off": "Apagado", + "language": "Lenguaje" + } }, "layout": { "interface": "Interfaz", @@ -137,7 +159,7 @@ "dash": "Modo escritorio", "silenceSkipper": "Omitir silencio (Experimental)", "youtubeJsFallback": "Procesar video localmente", - "youtubeJsAlways": "Utilizar siempre el procesamiento de vídeo local", + "youtubeJsAlways": "Utilizar siempre", "lockOrientation": "Bloquear orientación de pantalla" }, "bookmarklet": "Favoritos", @@ -158,7 +180,86 @@ "title": "DeArrow", "thumbnailInstanceUrl": "URL de la instancia de la miniatura", "titleOnly": "Solo títulos" + }, + "engine": "Motor de backend", + "about": "Acerca de", + "materialiousAccount": "Cuenta", + "shareURL": "Compartir URL", + "companionUrl": "URL del Companion", + "deleteAccount": "Borrar cuenta", + "clickXmoreTimesToDelete": "Haga clic {{clicksTillDelete}} veces más para borrar", + "historyEnabled": "Guardar historial de visionado", + "expandChapters": "Expandir capítulos por defecto", + "allowInsecureRequests": "Permitir peticiones inseguras", + "disableAutoUpdate": "Desabilitar actualizaciones automáticas", + "androidNativeShare": "Usar el \"compartir\" nativo", + "backendEngine": { + "warning": "Opciones avanzadas! Solo para usuarios avanzados. Se recomienda no cambiar nada, excepto que entiendas lo que haces.", + "cull": "Elementos máximos en el feed" } }, - "subscribe": "Suscríbete" + "subscribe": "Suscríbete", + "disabled": "Desactivado", + "popularPageDisabled": "La página popular ha sido desactivado por los Administradores", + "premium": "El contenido de YouTube Premium no puede ser visualizado en Materialious.", + "copy": "Copiar al portapapeles", + "login": "Acceder", + "username": "Nombre de usuario", + "password": "Contraseña", + "captionStyle": "Estilo de los subtítulos", + "invalidPassword": "Contraseña erronea", + "usernameTaken": "El nombre de usuario ya existe", + "linkInvidious": "Conecta una cuenta de Invidious", + "unlinkInvidious": "Desconecta una cuenta de Invidious", + "createAccount": "Crear cuenta", + "needRegister": "Necesito registrarme", + "registrationDisabled": "Registro desabilitado, contacte con el dueño de la instancia.", + "needLogin": "Necesito haber iniciado sesión", + "recommendedVideos": "Videos Recomendados", + "playlistVideos": "Videos en Lista de Reproducción", + "invidiousLogin": "Por favor, accede con tu cuenta de Invidious.", + "materialiousLogin": "Por favor, accede con tu cuenta de Materialious.", + "materialiousCreate": "Por favor, crea una cuenta de Materialious.", + "invalidInstance": "Por favor, verifica la URL. Si es correcta, puede que la instancia esté inactiva o no soporte clientes externos.", + "backend": "Backend", + "filters": { + "filters": "Filtros", + "type": "Tipos", + "uploadDate": "Fecha de subida", + "duration": "Duración", + "sortBy": "Ordernar por", + "features": "Características" + }, + "initalSetup": { + "useInvidious": "Quiero usar Invidious", + "useLocalFallback": "Quiero usar la opción de video local", + "yes": "Sí", + "no": "No", + "unsure": "No estoy seguro", + "required": "Configuración requerida", + "invidiousInfo": "Invidious es una interfaz auto-hospedable para YouTube.", + "localFallbackInfo": "Cuando Invidious falla en cargar un video, Materialious lo hace localmente.", + "configureInstance": "Configurar instancia", + "done": "Hecho" + }, + "skip": "Omitir", + "upcomingSegment": "Próximo ", + "comments": "comentarios", + "reply": "responder", + "contributors": "Colaboradores", + "version": "Versión", + "newest": "Nuevo", + "oldest": "Antiguo", + "popular": "Popular", + "watchParty": { + "start": "Empezar a ver en grupo", + "header": "Ver en grupo", + "createRoom": "Crear sala", + "joinRoom": "Unirse a una sala", + "roomID": "ID de la sala", + "shareRoom": "Compartir sala", + "leaveRoom": "Abandonar sala", + "userJoin": "Un usuario se ha unido a la sala", + "userLeft": "Un usuario ha abandonado la sala" + } } diff --git a/materialious/src/lib/letterCasing.ts b/materialious/src/lib/letterCasing.ts index 4031acd0..def900ba 100644 --- a/materialious/src/lib/letterCasing.ts +++ b/materialious/src/lib/letterCasing.ts @@ -1,8 +1,20 @@ import { get } from 'svelte/store'; import { interfaceForceCase } from './store'; -export type TitleCase = 'uppercase' | 'lowercase' | 'sentence case' | 'title case' | null; -export const titleCases: TitleCase[] = ['lowercase', 'uppercase', 'title case', 'sentence case']; +export type TitleCase = + | 'uppercase' + | 'lowercase' + | 'sentence case' + | 'title case' + | 'original' + | null; +export const titleCases: TitleCase[] = [ + 'original', + 'lowercase', + 'uppercase', + 'title case', + 'sentence case' +]; export function letterCase(text: string, caseTypeOverwrite?: TitleCase): string { const casing = caseTypeOverwrite ? caseTypeOverwrite : get(interfaceForceCase); diff --git a/materialious/src/lib/store.ts b/materialious/src/lib/store.ts index 28f2b4a3..075f43d2 100644 --- a/materialious/src/lib/store.ts +++ b/materialious/src/lib/store.ts @@ -214,7 +214,7 @@ export const interfaceSearchSuggestionsStore = persist( 'searchSuggestions' ); export const interfaceForceCase: Writable = persist( - writable(null), + writable('original'), createStorage(), 'forceCase' ); diff --git a/materialious/src/lib/thumbnail.ts b/materialious/src/lib/thumbnail.ts index 8e7a5ebf..c8b5fc19 100644 --- a/materialious/src/lib/thumbnail.ts +++ b/materialious/src/lib/thumbnail.ts @@ -1,9 +1,23 @@ -import type { Image } from './api/model'; +import type { + Image, + Notification, + PlaylistPageVideo, + Video, + VideoBase, + VideoWatchHistory +} from './api/model'; import { localDb } from './dexie'; import { getBestThumbnail } from './images'; let isInitial = false; +export type ThumbnailVideo = + | VideoBase + | Video + | Notification + | PlaylistPageVideo + | VideoWatchHistory; + export async function associateAvatar(channelId: string, avatars: Image[]) { if (avatars.length === 0) return; diff --git a/materialious/src/routes/(app)/playlist/[slug]/+page.svelte b/materialious/src/routes/(app)/playlist/[slug]/+page.svelte index 999c74f6..bda6a485 100644 --- a/materialious/src/routes/(app)/playlist/[slug]/+page.svelte +++ b/materialious/src/routes/(app)/playlist/[slug]/+page.svelte @@ -12,7 +12,7 @@
    - {#if data.playlist.videos} + {#if data.playlist.videos.length > 0}