Merge pull request #1549 from Materialious/update/1.16.17

Update/1.16.17
This commit is contained in:
Ward
2026-03-09 21:35:37 +13:00
committed by GitHub
16 changed files with 120 additions and 61 deletions
+2 -2
View File
@@ -7,8 +7,8 @@ android {
applicationId "us.materialio.app"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 233
versionName "1.16.16"
versionCode 234
versionName "1.16.17"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
@@ -75,7 +75,11 @@
<release version="1.16.16" date="2026-3-08">
<release version="1.16.17" date="2026-3-09">
<url>https://github.com/Materialious/Materialious/releases/tag/1.16.17</url>
</release>
<release version="1.16.16" date="2026-3-08">
<url>https://github.com/Materialious/Materialious/releases/tag/1.16.16</url>
</release>
<release version="1.16.15" date="2026-3-08">
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "Materialious",
"version": "1.16.16",
"version": "1.16.17",
"description": "Modern material design for YouTube and Invidious.",
"author": {
"name": "Ward Pearce",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "materialious",
"version": "1.16.16",
"version": "1.16.17",
"private": true,
"scripts": {
"dev": "npm run patch:github && vite dev",
@@ -118,6 +118,16 @@ export async function deleteWatchHistoryBackend() {
await fetch('/api/user/history', { method: 'DELETE' });
}
export async function deleteWatchHistoryItemBackend(videoId: string) {
await sodium.ready;
const rawKey = await getRawKey();
if (!rawKey) return;
const videoHash = await getSecureHash(videoId, rawKey);
await fetch(`/api/user/history/${videoHash}`, { method: 'DELETE' });
}
export async function saveWatchHistoryBackend(video: VideoPlay, progress: number = 0) {
await sodium.ready;
const rawKey = await getRawKey();
+9
View File
@@ -74,6 +74,7 @@ import {
} from './invidious/playlist';
import {
deleteWatchHistoryBackend,
deleteWatchHistoryItemBackend,
getVideoWatchHistoryBackend,
getWatchHistoryBackend,
saveWatchHistoryBackend,
@@ -343,6 +344,14 @@ export async function deleteWatchHistory() {
await localDb.watchHistory.clear();
}
export async function deleteWatchHistoryItem(videoId: string) {
if (isOwnBackend()?.internalAuth && get(rawMasterKeyStore)) {
return deleteWatchHistoryItemBackend(videoId);
}
await localDb.watchHistory.delete({ videoId });
}
export async function updateWatchHistory(
videoId: string,
progress: number,
@@ -60,7 +60,7 @@
<nav style="gap: 0.5em;">
<img class="circle large" {...avatar.image} alt="Channel profile" />
<button
class="large secondary-container"
class="circle large secondary-container"
{...mergeAttrs(avatar.fallback, {
style: 'text-transform: uppercase;border-radius: 2.5rem !important;'
})}>{channel.author[0]}</button
@@ -21,7 +21,7 @@
<img class="circle small" {...avatar.image} alt="Channel profile" />
<button
class="secondary-container"
class="circle secondary-container"
{...mergeAttrs(avatar.fallback, {
style: 'text-transform: uppercase;border-radius: 2.5rem !important;'
})}>{author[0]}</button
@@ -1,7 +1,5 @@
<script lang="ts">
import { _ } from '$lib/i18n';
import { removePlaylistVideo } from '$lib/api';
import { invidiousAuthStore, feedLastItemId, isAndroidTvStore } from '$lib/store';
import { feedLastItemId, isAndroidTvStore } from '$lib/store';
import ContentColumn from '$lib/components/layout/ContentColumn.svelte';
import { onMount } from 'svelte';
import Thumbnail from '$lib/components/thumbnail/VideoThumbnail.svelte';
@@ -18,21 +16,10 @@
interface Props {
items?: FeedItems;
playlistId?: string;
playlistAuthor?: string;
classes?: string;
}
let {
items = [],
playlistId = '',
playlistAuthor = '',
classes = 'page right active'
}: Props = $props();
async function removePlaylistItem(indexId: string) {
if (!playlistId) return;
await removePlaylistVideo(playlistId, indexId);
}
let { items = [], playlistId = '', classes = 'page right active' }: Props = $props();
onMount(async () => {
if (!$feedLastItemId) return;
@@ -99,17 +86,6 @@
{#key item.videoId}
<Thumbnail video={item} {playlistId} />
{/key}
{#if $invidiousAuthStore && decodeURIComponent($invidiousAuthStore.username) === playlistAuthor && 'indexId' in item}
<div class="right-align" style="margin: 1em .5em;">
<button
onclick={async () => removePlaylistItem(item.indexId)}
class="tertiary circle small"
>
<i>delete</i>
<div class="tooltip">{$_('delete')}</div>
</button>
</div>
{/if}
{:else if item.type === 'channel'}
<ChannelThumbnail channel={item} />
{:else if item.type === 'playlist'}
@@ -93,6 +93,8 @@
// Check if sideways should be enabled or disabled.
disableSideways();
addEventListener('resize', disableSideways);
if (!page.url.pathname.endsWith('/history'))
queueGetWatchHistory(video.videoId).then((watchHistory) => {
if (watchHistory) {
@@ -110,7 +112,7 @@
}
</script>
<div class:sideways-root={sideways} tabindex="0" role="button">
<div class:sideways-root={sideways} class:use-flex-column={!sideways} tabindex="0" role="button">
<div id="thumbnail-container">
<!-- eslint-disable svelte/no-navigation-without-resolve -->
<a
@@ -168,8 +170,8 @@
</a>
</div>
<div class="thumbnail-details video-title">
<div class="video-title">
<div class="thumbnail-details video-title" style="flex: 1;">
<div class="video-title use-flex-column">
<a
tabindex="-1"
style="padding-left: 1px;"
@@ -178,10 +180,10 @@
href={watchUrl.toString()}
onclick={onVideoSelected}
>
<span class="bold">{letterCase(video.title.trimEnd())}</span>
<span class="bold" style="width: 100%;">{letterCase(video.title.trimEnd())}</span>
</a>
<nav>
<nav class="align-end">
{#if !sideways && 'authorId' in video}
<AuthorAvatar author={video.author} authorId={video.authorId} />
{/if}
@@ -240,6 +242,12 @@
margin-bottom: -22%;
}
.use-flex-column {
height: 100%;
display: flex;
flex-direction: column;
}
.thumbnail {
width: 100%;
overflow: hidden;
@@ -307,12 +315,21 @@
filter: brightness(40%);
}
@media screen and (max-width: 1499px) {
.align-end {
flex: 1;
align-items: end;
}
@media screen and (max-width: 1800px) {
.sideways-root .thumbnail {
width: 100%;
max-height: 100%;
}
.align-end {
align-items: center;
}
.sideways-root .thumbnail-details {
display: none;
}
+43 -9
View File
@@ -104,7 +104,7 @@
"defaultLanguage": "既定の言語",
"retryText": "動画の読み込みに失敗。どう続けますか?",
"enableYoutubejsTemp": "一度だけ動画への直接接続に切り替え",
"enableYoutubejsPerm": "常に動画への接続エラーで直接接続を許可",
"enableYoutubejsPerm": "ローカル動画へのフォールバックを常に許可",
"youtubeJsFallBack": "動画接続エラー 直接接続に切替",
"youtubeJsLoading": "動画の読み込みに失敗。動画への直接接続に切り替えます。",
"premiere": "動画は利用可能です",
@@ -163,12 +163,12 @@
"proxyVideos": "動画のプロキシ",
"backgroundPlay": "バックグラウンドで音声を再生",
"dash": "DASH",
"listenByDefault": "最初から音声モード",
"silenceSkipper": "無音部分を飛ばす (実験的)",
"listenByDefault": "既定で音声再生",
"silenceSkipper": "無音部分をスキップ (実験的機能)",
"localVideoFallback": "動画の接続エラーで直接接続に切替",
"youtubeJsFallback": "失敗時に動画の代替処理を使用",
"youtubeJsAlways": "常に使用",
"lockOrientation": "アスペクト比向きを固定",
"lockOrientation": "アスペクト比に合わせて向きを固定",
"defaultPlaybackSpeed": "既定の再生速度",
"playerStatistics": "最初からプレイヤー統計情報を表示",
"networkChangePause": "ネットワーク変更時に一時停止"
@@ -178,7 +178,7 @@
"sponsors": {
"sponsor": "協賛",
"disableToast": "通知を非表示",
"disableTimeline": "タイムライン上のシーン表示を非表示",
"disableTimeline": "タイムライン上のセグメントを無効化",
"Catagories": "分類",
"unpaidSelfPromotion": "無償または自己の宣伝",
"interactionReminder": "登録など操作を頼む自己宣伝",
@@ -213,7 +213,7 @@
"cooldown": "チャンネル時間制限の再取得",
"concurrent": "フィードが返される前に解析する最大チャンネル数",
"fallbacks": "フォールバック",
"importExport": "サブスクリプションのインポート/エクスポート",
"importExport": "自動エクスポート/インポート",
"exportToInvidious": "MaterialiousからInvidiousにエクスポート",
"importToMaterialious": "Invidious から Materialious へのインポート",
"importingToMaterialious": "マテリアルへのインポートを開始しました",
@@ -222,9 +222,32 @@
"exportingToInvidiousFinished": "Invidiousへのエクスポートが終了しました"
},
"exportToJson": "JSON にエクスポート",
"historyEnabled": "視聴履歴を保存"
"historyEnabled": "視聴履歴を保存",
"filter": {
"title": "コンテンツフィルター",
"url": "URLからフィルターをインポート",
"autoUpdate": "URLからフィルターを自動更新する",
"contentType": "コンテンツタイプ",
"field": "フィールド",
"operator": "オペレーター",
"optionPlaceholder": "オプションの選択",
"value": "値",
"addConditional": "条件を追加",
"addFilter": "フィルターを追加"
},
"export": {
"title": "エクスポート/インポート",
"importSub": "登録チャンネルをインポート",
"importSubSupported": "YouTube CSV/OPML/JSON、Invidious OPML/JSON、Freetube データベース、NewPipe JSON をサポート",
"importing": "登録チャンネルをインポートしています (少し時間がかかります)",
"importingCompleted": "登録チャンネルを正常にインポートしました",
"exportSub": "登録チャンネルをエクスポート",
"exportToClipboard": "クリップボードへエクスポート",
"exportToFile": "ファイルへエクスポート",
"importFromFile": "ファイルからインポート"
}
},
"subscribe": "登録",
"subscribe": "チャンネル登録",
"invidiousBlockWarning": "Invidious は、現在Googleにブロックされています。このインスタンスで動画が読み込めない場合、{android} 版または {desktop} 版の Materialious でこのインスタンスを使用し、動画取得を直接行う代替接続を使ってください。",
"reply": "返信",
"comments": "コメント",
@@ -248,7 +271,7 @@
"popular": "人気",
"favouriteChannel": "お気に入りチャンネル",
"unfavouriteChannel": "お気に入り解除チャンネル",
"copy": "JSON をコピー",
"copy": "クリップボードにコピー",
"username": "ユーザー名",
"password": "パスワード",
"captionStyle": "見出しのスタイル",
@@ -274,5 +297,16 @@
"localFallbackInfo": "Invidious からのビデオの読み込みに失敗した場合、Materialious はそれをローカルに読み込みます。",
"configureInstance": "インスタンスの構成",
"done": "完了"
},
"watchParty": {
"start": "ウォッチパーティーを開始",
"header": "ウォッチパーティー",
"createRoom": "ルームを作成",
"joinRoom": "ルームに参加",
"roomID": "ルーム ID",
"shareRoom": "ルームを共有",
"leaveRoom": "ルームから退出",
"userJoin": "ユーザーがルームに参加しました",
"userLeft": "ユーザーがルームから退出しました"
}
}
@@ -97,9 +97,5 @@
<div class="space"></div>
{#if data.playlist.videos}
<ItemsList
items={data.playlist.videos}
playlistAuthor={data.playlist.info.author}
playlistId={data.playlist.info.playlistId}
/>
<ItemsList items={data.playlist.videos} playlistId={data.playlist.info.playlistId} />
{/if}
@@ -1,11 +1,12 @@
import { getSubscriptions } from '$lib/api';
import type { Subscription } from '$lib/api/model';
import { authProtected } from '$lib/auth';
import { error } from '@sveltejs/kit';
export async function load() {
authProtected();
let subscriptions;
let subscriptions: Subscription[];
try {
subscriptions = await getSubscriptions();
@@ -13,6 +14,8 @@ export async function load() {
error(500, errorMessage);
}
subscriptions.sort((a, b) => a.author.localeCompare(b.author));
return {
subscriptions: subscriptions
};
@@ -100,14 +100,24 @@ async function proxyRequest(
}
}
const requestHeaders = new Headers();
const requestHeaders = new Headers(request.headers);
requestHeaders.set('host', urlToProxyObj.host);
requestHeaders.set('origin', urlToProxyObj.origin);
requestHeaders.set('user-agent', USER_AGENT);
const authHeader = request.headers.get('Authorization');
if (authHeader) {
requestHeaders.set('Authorization', authHeader);
// Remove headers that may cause issues or be auto-managed
for (const key of [
'referer',
'x-forwarded-for',
'x-requested-with',
'sec-ch-ua-mobile',
'sec-ch-ua',
'sec-ch-ua-platform',
'cookie', // Ensure auth cookies don't become included.
'content-type',
'content-length'
]) {
requestHeaders.delete(key);
}
const requestOptions: RequestInit = {
@@ -119,7 +129,7 @@ async function proxyRequest(
let body;
if (request.body && request.headers.has('__is_base64_encoded')) {
request.headers.delete('__is_base64_encoded');
requestHeaders.delete('__is_base64_encoded');
await sodium.ready;
body = Uint8Array.from(sodium.from_base64(await request.text()));
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -3,7 +3,7 @@ import os
import re
from datetime import datetime
LATEST_VERSION = "1.16.16"
LATEST_VERSION = "1.16.17"
RELEASE_DATE = datetime.now().strftime("%Y-%-m-%d") # Format: YYYY-M-D
WORKING_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "materialious")