Merge pull request #850 from Materialious/update/1.7.17

Update/1.7.17
This commit is contained in:
Ward
2025-04-03 14:26:59 +13:00
committed by GitHub
14 changed files with 553 additions and 229 deletions
+1 -91
View File
@@ -273,94 +273,4 @@ Add these additional environment variables to Materialious.
VITE_DEFAULT_PEERJS_HOST: "peerjs.example.com"
VITE_DEFAULT_PEERJS_PATH: "/"
VITE_DEFAULT_PEERJS_PORT: 443
```
## Step 7 (Optional): Enabling downloads
### Step 1: Add the following to your reverse proxies
#### Caddy
```caddy
# Materialious
materialious.example.com {
reverse_proxy localhost:3001
header {
Cross-Origin-Opener-Policy "same-origin"
Cross-Origin-Embedder-Policy "require-corp"
}
}
# Invidious
invidious.example.com {
# Other configurations
header /ggpht/* Cross-Origin-Resource-Policy cross-origin
}
```
#### Nginx
```nginx
# Materialious
server {
listen 80;
server_name materialious.example.com;
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Embedder-Policy "require-corp" always;
location / {
proxy_pass http://localhost:3001;
}
}
# Invidious
server {
listen 80;
server_name invidious.example.com;
location /ggpht/ {
add_header Cross-Origin-Resource-Policy cross-origin;
}
}
```
#### Traefik
Add the following to Materialious
```yaml
# Materialious
http:
middlewares:
materialious-downloads:
headers:
customResponseHeaders:
Cross-Origin-Opener-Policy: "same-origin"
Cross-Origin-Embedder-Policy: "require-corp"
# Invidious
http:
routers:
ggpht-router:
rule: "PathPrefix(`/ggpht/`)"
service: ggpht-service
middlewares:
- headers-ggpht
middlewares:
headers-ggpht:
headers:
customResponseHeaders:
Cross-Origin-Resource-Policy: cross-origin
```
### Step 2: Modify docker compose.
Add this environment variable to Materialious.
```yaml
VITE_DEFAULT_DOWNLOAD_ENABLED: true
```
```
+2 -2
View File
@@ -7,8 +7,8 @@ android {
applicationId "us.materialio.app"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 98
versionName "1.7.16"
versionCode 99
versionName "1.7.17"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
@@ -59,7 +59,11 @@
<release version="1.7.16" date="2025-4-02">
<release version="1.7.17" date="2025-4-02">
<url>https://github.com/Materialious/Materialious/releases/tag/1.7.17</url>
</release>
<release version="1.7.16" date="2025-4-02">
<url>https://github.com/Materialious/Materialious/releases/tag/1.7.16</url>
</release>
<release version="1.7.15" date="2025-2-23">
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "Materialious",
"version": "1.7.16",
"version": "1.7.17",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "Materialious",
"version": "1.7.16",
"version": "1.7.17",
"license": "MIT",
"dependencies": {
"@capacitor-community/electron": "^5.0.0",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "Materialious",
"version": "1.7.16",
"version": "1.7.17",
"description": "Modern material design for Invidious.",
"author": {
"name": "Ward Pearce",
@@ -44,4 +44,4 @@
"capacitor",
"electron"
]
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "materialious",
"version": "1.7.16",
"version": "1.7.17",
"private": true,
"scripts": {
"dev": "vite dev",
+76 -70
View File
@@ -1,11 +1,10 @@
<script lang="ts">
import Comment_1 from './Comment.svelte';
import { getComments } from '$lib/api';
import { type Comment, type Comments } from '$lib/api/model';
import { getBestThumbnail, proxyGoogleImage } from '$lib/images';
import { interfaceLowBandwidthMode } from '$lib/store';
import { numberWithCommas } from '$lib/time';
import { _ } from 'svelte-i18n';
import CommentSelf from './Comment.svelte';
interface Props {
comment: Comment;
@@ -16,6 +15,8 @@
let replies: Comments | undefined = $state(undefined);
const replyText: string = comment.replies?.replyCount > 1 ? 'replies' : 'reply';
async function loadReplies(continuation: string) {
try {
replies = await getComments(videoId, {
@@ -39,91 +40,96 @@
<div class="comment">
{#if !$interfaceLowBandwidthMode}
<img
class="circle small"
src={proxyGoogleImage(getBestThumbnail(comment.authorThumbnails))}
alt="comment profile"
/>
{/if}
<div>
<div class="row">
<a href={`/channel/${comment.authorId}`}>
<p class="no-margin">
<span class="bold" class:channel-owner={comment.authorIsChannelOwner}
>{comment.author}</span
>
<div class="comment-header">
<img
class="circle small"
src={proxyGoogleImage(getBestThumbnail(comment.authorThumbnails))}
alt="comment profile"
/>
<div class="comment-info">
<a href={`/channel/${comment.authorId}`} class="author">
<span class="bold" class:channel-owner={comment.authorIsChannelOwner}>
{comment.author}
</span>
<span class="secondary-text">{comment.publishedText}</span>
</a>
<p class="no-margin">
{@html commentTimestamps(comment.contentHtml)}
</p>
</a>
{#if comment.isPinned}
<i>push_pin</i>
{/if}
{#if comment.isEdited}
<i>edit</i>
{/if}
</div>
<p class="no-margin">
{@html commentTimestamps(comment.contentHtml)}
</p>
<div style="display: flex;">
<p><i>thumb_up</i> {numberWithCommas(comment.likeCount)}</p>
{#if comment.creatorHeart}
<div>
<img
class="circle"
style="width: 25px; height: 25px"
src={proxyGoogleImage(comment.creatorHeart.creatorThumbnail)}
alt="Creator profile"
/>
<i style="font-size: 20px;margin-left: 5px;" class="absolute left red-text bottom fill"
>favorite</i
>
<div class="comment-actions">
<p class="no-margin no-padding"><i>thumb_up</i> {numberWithCommas(comment.likeCount)}</p>
{#if comment.replies && !replies}
<button
onclick={async () => loadReplies(comment.replies.continuation)}
class="transparent replies"
>
<i class="white-text">expand_more</i>
<span class="white-text">{comment.replies.replyCount} {replyText}</span>
</button>
{:else if replies}
<button onclick={() => (replies = undefined)} class="transparent replies">
<i class="white-text">expand_less</i>
<span class="white-text">Hide {replyText}</span>
</button>
{/if}
</div>
{/if}
</div>
</div>
{/if}
{#if replies}
{#if replies}
<div style="margin-left: 5em;">
{#each replies.comments as reply}
<Comment_1 comment={reply} {videoId} />
<CommentSelf comment={reply} {videoId} />
{/each}
{/if}
{#if comment.replies && !replies}
<button
onclick={async () => loadReplies(comment.replies.continuation)}
class="transparent replies"
>
<i class="primary-text">expand_more</i>
<span class="primary-text">{comment.replies.replyCount} {$_('replies')}</span>
</button>
{:else if replies}
<button onclick={() => (replies = undefined)} class="transparent replies">
<i class="primary-text">expand_less</i>
<span class="primary-text">{$_('hideReplies')}</span>
</button>
{/if}
</div>
</div>
{/if}
</div>
<style>
.comment img {
margin: 0.5em 1em 0 1em;
}
.comment {
display: flex;
margin-bottom: 0.8em;
flex-direction: column;
margin-bottom: 1em;
white-space: pre-line;
}
.channel-owner {
background-color: var(--primary);
padding: 0 0.5em;
border-radius: 1em;
color: var(--surface-variant);
.comment-header {
display: flex;
align-items: flex-start;
}
.comment-header img {
margin: 0;
border-radius: 50%;
}
.comment-info {
display: flex;
flex-direction: column;
margin-left: 1em;
}
.author {
display: flex;
align-items: flex-start;
gap: 6px;
font-size: 14px;
}
.comment-actions {
display: flex;
align-items: center;
gap: 15px;
margin-top: 5px;
font-size: 14px;
}
a {
justify-content: start;
}
.replies {
margin-left: 0;
padding: 0.1em;
}
</style>
+124 -43
View File
@@ -1,5 +1,6 @@
<script lang="ts">
import { page } from '$app/stores';
import '$lib/css/shaka-player-theme.css';
import { getBestThumbnail } from '$lib/images';
import { padTime, videoLength } from '$lib/time';
import { Capacitor } from '@capacitor/core';
@@ -60,6 +61,51 @@
let player: shaka.Player;
let shakaUi: shaka.ui.Overlay;
const STORAGE_KEY_QUALITY = 'shaka-preferred-quality';
const STORAGE_KEY_VOLUME = 'shaka-preferred-volume';
function saveQualityPreference() {
const tracks = player.getVariantTracks();
const selectedTrack = tracks.find((track) => track.active);
if (selectedTrack) {
localStorage.setItem(STORAGE_KEY_QUALITY, selectedTrack.bandwidth.toString());
}
}
function saveVolumePreference() {
localStorage.setItem(STORAGE_KEY_VOLUME, playerElement.volume.toString());
}
function restorePreferences() {
const savedQuality =
localStorage.getItem(STORAGE_KEY_QUALITY) ??
(import.meta.env.VITE_DEFAULT_DASH_BITRATE as string);
if (savedQuality) {
const qualityBandwidth = parseInt(savedQuality, 10);
const tracks = player.getVariantTracks();
let preferredTrack = tracks.find((track) => track.bandwidth === qualityBandwidth);
if (!preferredTrack) {
preferredTrack = tracks.reduce((prev, curr) =>
Math.abs(curr.bandwidth - qualityBandwidth) < Math.abs(prev.bandwidth - qualityBandwidth)
? curr
: prev
);
}
if (preferredTrack) {
player.selectVariantTrack(preferredTrack, true);
player.configure({ abr: { enabled: false } });
}
}
const savedVolume = localStorage.getItem(STORAGE_KEY_VOLUME);
if (savedVolume) {
playerElement.volume = parseFloat(savedVolume);
}
}
function loadTimeFromUrl(page: Page): boolean {
if (player) {
const timeGivenUrl = page.url.searchParams.get('time');
@@ -92,6 +138,9 @@
playerElement
);
player.addEventListener('adaptation', () => setTimeout(saveQualityPreference, 10000));
playerElement.addEventListener('volumechange', saveVolumePreference);
shakaUi.configure({
controlPanelElements: [
'play_pause',
@@ -194,56 +243,73 @@
};
if (type == shaka.net.NetworkingEngine.RequestType.SEGMENT) {
const googUmp = new GoogleVideo.UMP(
new GoogleVideo.ChunkedDataBuffer([new Uint8Array(response.data as ArrayBuffer)])
);
const url = new URL(response.uri);
let redirect: Protos.SabrRedirect | undefined;
// Fix positioning for auto-generated subtitles
if (
url.hostname.endsWith('.youtube.com') &&
url.pathname === '/api/timedtext' &&
url.searchParams.get('caps') === 'asr' &&
url.searchParams.get('kind') === 'asr' &&
url.searchParams.get('fmt') === 'vtt'
) {
const stringBody = new TextDecoder().decode(response.data);
// position:0% for LTR text and position:100% for RTL text
const cleaned = stringBody.replaceAll(/ align:start position:(?:10)?0%$/gm, '');
googUmp.parse((part) => {
try {
const data = part.data.chunks[0];
switch (part.type) {
case 20: {
const mediaHeader = Protos.MediaHeader.decode(data);
console.info('[MediaHeader]:', mediaHeader);
break;
}
case 21: {
handleMediaData(part.data.split(1).remainingBuffer.chunks[0]);
break;
}
case 43: {
redirect = Protos.SabrRedirect.decode(data);
console.info('[SABRRedirect]:', redirect);
break;
}
case 58: {
const streamProtectionStatus = Protos.StreamProtectionStatus.decode(data);
switch (streamProtectionStatus.status) {
case 1:
console.info('[StreamProtectionStatus]: Ok');
break;
case 2:
console.error('[StreamProtectionStatus]: Attestation pending');
break;
case 3:
console.error('[StreamProtectionStatus]: Attestation required');
break;
default:
break;
response.data = new TextEncoder().encode(cleaned).buffer as ArrayBuffer;
} else {
const googUmp = new GoogleVideo.UMP(
new GoogleVideo.ChunkedDataBuffer([new Uint8Array(response.data as ArrayBuffer)])
);
let redirect: Protos.SabrRedirect | undefined;
googUmp.parse((part) => {
try {
const data = part.data.chunks[0];
switch (part.type) {
case 20: {
const mediaHeader = Protos.MediaHeader.decode(data);
console.info('[MediaHeader]:', mediaHeader);
break;
}
case 21: {
handleMediaData(part.data.split(1).remainingBuffer.chunks[0]);
break;
}
case 43: {
redirect = Protos.SabrRedirect.decode(data);
console.info('[SABRRedirect]:', redirect);
break;
}
case 58: {
const streamProtectionStatus = Protos.StreamProtectionStatus.decode(data);
switch (streamProtectionStatus.status) {
case 1:
console.info('[StreamProtectionStatus]: Ok');
break;
case 2:
console.error('[StreamProtectionStatus]: Attestation pending');
break;
case 3:
console.error('[StreamProtectionStatus]: Attestation required');
break;
default:
break;
}
break;
}
break;
}
} catch (error) {
console.error('An error occurred while processing the part:', error);
}
} catch (error) {
console.error('An error occurred while processing the part:', error);
}
});
});
if (redirect) return handleRedirect(redirect);
if (redirect) return handleRedirect(redirect);
if (mediaData.length) response.data = mediaData;
if (mediaData.length) response.data = mediaData;
}
}
});
}
@@ -398,6 +464,20 @@
snackBarAlert = get(_)('player.youtubeJsFallBack');
ui('#snackbar-alert');
}
restorePreferences();
const overflowMenuButton = document.querySelector('.shaka-overflow-menu-button');
if (overflowMenuButton) {
overflowMenuButton.innerHTML = 'settings';
}
const backToOverflowButton = document.querySelector(
'.shaka-back-to-overflow-button .material-icons-round'
);
if (backToOverflowButton) {
backToOverflowButton.innerHTML = 'arrow_back_ios_new';
}
});
async function loadPlayerPos() {
@@ -482,6 +562,7 @@
<div
id="shaka-container"
class="youtube-theme"
style="max-height: 80vh; max-width: calc(80vh * 16 / 9); overflow: hidden; position: relative; flex: 1; background-color: black;"
data-shaka-player-container
>
+3 -1
View File
@@ -26,15 +26,17 @@ article, dialog {
}
button.row {
padding: 0 !important;
background-color: transparent !important;
border: none !important;
color: var(--on-surface) !important;
width: 100%;
margin: 0 !important;
}
button.row:hover {
box-shadow: none !important;
background-color: transparent !important;
width: 100%;
}
@media screen and (max-width: 650px) {
@@ -0,0 +1,314 @@
.youtube-theme {
font-family: 'Roboto', sans-serif;
}
.youtube-theme .shaka-bottom-controls {
width: 100%;
padding: 0;
padding-bottom: 0;
z-index: 1;
}
.youtube-theme .shaka-bottom-controls {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-ms-flex-direction: column;
flex-direction: column;
}
.youtube-theme .shaka-ad-controls {
-webkit-box-ordinal-group: 2;
-ms-flex-order: 1;
order: 1;
}
.youtube-theme .shaka-controls-button-panel {
-webkit-box-ordinal-group: 3;
-ms-flex-order: 2;
order: 2;
height: 40px;
padding: 0 10px;
}
.youtube-theme .shaka-range-container {
margin: 4px 10px 4px 10px;
top: 0;
}
.youtube-theme .shaka-small-play-button {
-webkit-box-ordinal-group: -2;
-ms-flex-order: -3;
order: -3;
}
.youtube-theme .shaka-mute-button {
-webkit-box-ordinal-group: -1;
-ms-flex-order: -2;
order: -2;
}
.youtube-theme .shaka-controls-button-panel>* {
margin: 0;
padding: 3px 8px;
color: var(--primary-text);
height: 40px;
}
.youtube-theme .shaka-controls-button-panel>*:focus {
outline: none;
-webkit-box-shadow: inset 0 0 0 2px var(--primary);
box-shadow: inset 0 0 0 2px var(--primary);
color: var(--surface-bright);
}
.youtube-theme .shaka-controls-button-panel>*:hover {
color: var(--surface-variant);
}
.youtube-theme .shaka-controls-button-panel .shaka-volume-bar-container {
position: relative;
z-index: 10;
left: -1px;
-webkit-box-ordinal-group: 0;
-ms-flex-order: -1;
order: -1;
opacity: 0;
width: 0px;
-webkit-transition: width 0.2s cubic-bezier(0.4, 0, 1, 1);
transition: width 0.2s cubic-bezier(0.4, 0, 1, 1);
height: 3px;
padding: 0;
}
.youtube-theme .shaka-controls-button-panel .shaka-volume-bar-container:hover,
.youtube-theme .shaka-controls-button-panel .shaka-volume-bar-container:focus {
display: block;
width: 50px;
opacity: 1;
padding: 0 6px;
}
.youtube-theme .shaka-mute-button:hover+div {
opacity: 1;
width: 50px;
padding: 0 6px;
}
.youtube-theme .shaka-current-time {
padding: 0 10px;
font-size: 12px;
}
.youtube-theme .shaka-seek-bar-container {
height: 3px;
position: relative;
top: -1px;
border-radius: 0;
margin-bottom: 0;
}
.youtube-theme .shaka-seek-bar-container .shaka-range-element {
opacity: 0;
}
.youtube-theme .shaka-seek-bar-container:hover {
height: 5px;
top: 0;
cursor: pointer;
}
.youtube-theme .shaka-seek-bar-container:hover .shaka-range-element {
opacity: 1;
cursor: pointer;
}
.youtube-theme .shaka-seek-bar-container input[type=range]::-webkit-slider-thumb {
background: var(--primary);
cursor: pointer;
}
.youtube-theme .shaka-seek-bar-container input[type=range]::-moz-range-thumb {
background: var(--primary);
cursor: pointer;
}
.youtube-theme .shaka-seek-bar-container input[type=range]::-ms-thumb {
background: var(--primary);
cursor: pointer;
}
.youtube-theme .shaka-video-container * {
font-family: 'Roboto', sans-serif;
}
.youtube-theme .shaka-overflow-menu,
.youtube-theme .shaka-settings-menu {
border-radius: 2px;
background: var(--surface-dim);
text-shadow: 0 0 2px rgb(0 0 0%);
-webkit-transition: opacity 0.1s cubic-bezier(0, 0, 0.2, 1);
transition: opacity 0.1s cubic-bezier(0, 0, 0.2, 1);
-moz-user-select: none;
-ms-user-select: none;
-webkit-user-select: none;
right: 10px;
bottom: 50px;
padding: 8px 0;
min-width: 200px;
}
.youtube-theme .shaka-settings-menu {
padding: 0 0 8px 0;
}
.youtube-theme .shaka-settings-menu button {
font-size: 12px;
}
.youtube-theme .shaka-settings-menu button span {
margin-left: 33px;
font-size: 13px;
}
.youtube-theme .shaka-settings-menu button[aria-selected="true"] {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
}
.youtube-theme .shaka-settings-menu button[aria-selected="true"] span {
-webkit-box-ordinal-group: 3;
-ms-flex-order: 2;
order: 2;
margin-left: 0;
}
.youtube-theme .shaka-settings-menu button[aria-selected="true"] i {
-webkit-box-ordinal-group: 2;
-ms-flex-order: 1;
order: 1;
font-size: 18px;
padding-left: 5px;
}
.youtube-theme .shaka-overflow-menu button {
padding: 0;
}
.youtube-theme .shaka-overflow-menu button i {
display: none;
}
.youtube-theme .shaka-overflow-menu button .shaka-overflow-button-label {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: justify;
-ms-flex-pack: justify;
justify-content: space-between;
-webkit-box-orient: horizontal;
-webkit-box-direction: normal;
-ms-flex-direction: row;
flex-direction: row;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
cursor: default;
outline: none;
height: 40px;
-webkit-box-flex: 0;
-ms-flex: 0 0 100%;
flex: 0 0 100%;
}
.youtube-theme .shaka-overflow-menu button .shaka-overflow-button-label span {
-ms-flex-negative: initial;
flex-shrink: initial;
padding-left: 15px;
font-size: 13px;
font-weight: 500;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
}
.youtube-theme .shaka-overflow-menu span+span {
color: var(--primary-text);
font-weight: 400 !important;
font-size: 12px !important;
padding-right: 8px;
padding-left: 0 !important;
}
.youtube-theme .shaka-overflow-menu .shaka-pip-button span+span {
padding-right: 15px !important;
}
.youtube-theme .shaka-overflow-menu .shaka-pip-button span+span:after {
content: "";
}
.youtube-theme .shaka-back-to-overflow-button {
padding: 8px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
font-size: 12px;
color: var(--primary-text);
height: 40px;
}
.youtube-theme .shaka-back-to-overflow-button .material-icons-round {
font-size: 15px;
padding-right: 10px;
}
.youtube-theme .shaka-back-to-overflow-button span {
margin-left: 3px !important;
}
.youtube-theme .shaka-overflow-menu button:hover,
.youtube-theme .shaka-settings-menu button:hover {
background-color: var(--surface-bright);
cursor: pointer;
}
.youtube-theme .shaka-overflow-menu button:hover label,
.youtube-theme .shaka-settings-menu button:hover label {
cursor: pointer;
}
.youtube-theme .shaka-overflow-menu button:focus,
.youtube-theme .shaka-settings-menu button:focus {
background-color: var(--surface-bright);
border: none;
outline: none;
}
.youtube-theme .shaka-overflow-menu button,
.youtube-theme .shaka-settings-menu button {
color: var(--primary-text);
}
.youtube-theme .shaka-captions-off {
color: var(--surface-bright);
}
.youtube-theme .shaka-overflow-menu-button {
font-size: 18px;
margin-right: 5px;
}
.youtube-theme .shaka-fullscreen-button:hover {
font-size: 25px;
-webkit-transition: font-size 0.1s cubic-bezier(0, 0, 0.2, 1);
transition: font-size 0.1s cubic-bezier(0, 0, 0.2, 1);
}
.youtube-theme .shaka-controls-button-panel>*:hover {
color: var(--primary) !important;
}
+3 -1
View File
@@ -239,7 +239,9 @@
{/if}
{#if mobileSearchShow}
<Search on:searchCancelled={() => (mobileSearchShow = false)} />
<div style="width: 100%;">
<Search on:searchCancelled={() => (mobileSearchShow = false)} />
</div>
{:else}
{#if !Capacitor.isNativePlatform()}
<button data-ui="#sync-party" class="circle large transparent">
@@ -89,21 +89,26 @@
<span>{$_('player.share.title')}</span>
<menu class="no-wrap mobile">
{#if !Capacitor.isNativePlatform()}
<a
href="#share"
<button
class="row"
onclick={async () => {
await Clipboard.write({ string: location.href });
}}>{$_('player.share.materialiousLink')}</a
}}>{$_('player.share.materialiousLink')}</button
>
{/if}
<a
href="#share"
onclick={async () => {
await Clipboard.write({
string: `https://www.youtube.com/playlist?list=${data.playlist.playlistId}`
});
}}>{$_('player.share.youtubeLink')}</a
>
<!--Silly hack so svelte doesnt error-->
{#if true}
<button
class="row"
onclick={async () => {
await Clipboard.write({
string: `https://www.youtube.com/playlist?list=${data.playlist.playlistId}`
});
}}
>
{$_('player.share.youtubeLink')}
</button>
{/if}
</menu>
</button>
</article>
@@ -540,8 +540,8 @@
<div class="tooltip">{$_('player.addToPlaylist')}</div>
<menu class="no-wrap mobile">
{#each personalPlaylists as personalPlaylist}
<a
href="#add"
<button
class="row"
onclick={async () => await toggleVideoToPlaylist(personalPlaylist.playlistId)}
>
<nav>
@@ -554,7 +554,7 @@
<i>add</i>
{/if}
</nav>
</a>
</button>
{/each}
</menu>
</button>
+1 -1
View File
@@ -3,7 +3,7 @@ import os
import re
from datetime import datetime
LATEST_VERSION = "1.7.16"
LATEST_VERSION = "1.7.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")