Add pwa support

This commit is contained in:
2026-02-28 06:32:39 -06:00
parent 02b10131f9
commit 28b7138547
25 changed files with 870 additions and 48 deletions

View File

@@ -20,11 +20,19 @@ func Components.PersonalListDescription(tytd,name,editing)
</form>
</true>
<false>
<dialog id="dialog">
<h5>The url</h5>
<div><a id="the_playlist_url" href=""></a></div>
<nav class="right-align no-space">
<button data-ui="#dialog" class="transparent link">OK</button>
</nav>
</dialog>
<div class="row" id="description">
<div class="max">
<plink(description)>
</div>
<div class="min">
<button onclick="getPersonalTempLink()"><i>link</i></button>
<button hx-get={$"./edit-personal-description?name={Net.Http.UrlEncode(name)}"} hx-target="#description" hx-swap="outerHTML"><i>edit</i></button>
</div>
</div>

View File

@@ -1,4 +1,4 @@
var progress=0;
func Components.Progress(tytd)
{
var vid = tytd.CurrentVideo;
@@ -8,6 +8,5 @@ func Components.Progress(tytd)
<h4><a hx-target="body" hx-push-url="true" hx-get={$"./channel?id={Net.Http.UrlEncode(vid.ChannelId)}"} href={$"./channel?id={Net.Http.UrlEncode(vid.ChannelId)}"}>{vid.Channel}</a></h4>
<progress class="wavy light-green-text" value={tytd.CurrentVideoProgress * 100.0} max="100" min="0"></progress>
</div>;
progress++;
return html;
}

View File

@@ -0,0 +1,12 @@
func Components.QueueSZ(tytd)
{
var vid = tytd.CurrentVideo;
tytd.Mutex.Lock();
var html = <span hx-trigger="every 5000ms" hx-target="this" hx-push-url="false" hx-indicator="none" hx-get="./queue-size" hx-swap="outerHTML" class="badge">{tytd.VideoQueueCount}</span>;
tytd.Mutex.Unlock();
return html;
}

View File

@@ -1,11 +1,21 @@
func Components.Shell(title, html, page, $mypage)
{
if(TypeOf(mypage) != "Path")
mypage = /;
var index = (/).MakeRelative(mypage).ToString();
if(index == "") index = "./";
const service_worker_script_path = (/"service_worker.js").MakeRelative(mypage).ToString();
const service_worker_script = $"<script>
if(\"serviceWorker\" in navigator) {123.ToChar()}
navigator.serviceWorker.register({Json.Encode(service_worker_script_path)})
.then(()=>console.log('Service worker registered'))
.catch(()=>console.log('service work not registered'));
{125.ToChar()}
</script>";
var pages = [
{
text="Home",
@@ -47,7 +57,11 @@ func Components.Shell(title, html, page, $mypage)
<title>TYTD - {title}</title>
<link rel="stylesheet" href={(/"beer.min.css").MakeRelative(mypage).ToString()}>
<link rel="stylesheet" href={(/"theme.css").MakeRelative(mypage).ToString()}>
<link rel="manifest" href={(/"site.webmanifest").MakeRelative(mypage).ToString()}>
<script src={(/"htmx.min.js").MakeRelative(mypage).ToString()} defer></script>
<script src={(/"offline.js").MakeRelative(mypage).ToString()} defer></script>
<script type="module" src={(/"beer.min.js").MakeRelative(mypage).ToString()} defer></script>
</head>
<body hx-indicator="#loading-indicator">
@@ -56,7 +70,15 @@ func Components.Shell(title, html, page, $mypage)
<each(var page : pages)>
<a hx-get={page.href} hx-target="body" hx-push-url="true" class={page.classStr}>
<i>{page.icon}</i>
<if(page.text == "Home")>
<true>
<span hx-trigger="every 1000ms" hx-target="this" hx-push-url="false" hx-indicator="none" hx-get="./queue-size" hx-swap="outerHTML" class="badge">
?
</span>
</true>
</if>
<div>{page.text}</div>
</a>
</each>
</nav>
@@ -69,7 +91,7 @@ func Components.Shell(title, html, page, $mypage)
<div class="htmx-indicator shape loading-indicator extra" id="loading-indicator">
<img class="responsive" src="./tytd.svg">
</div>
<raw(service_worker_script)>
</body>
</html>;
}

View File

@@ -1,3 +1,5 @@
const BUILD_TIME = comptime DateTime.NowEpoch;
var TYTDResources = [
{path="/beer.min.css",value=embed("beer.min.css")},
{path="/beer.min.js",value=embed("beer.min.js")},
@@ -8,12 +10,31 @@ var TYTDResources = [
{path="/htmx.min.js",value=embed("htmx.min.js")},
{path="/favicon.ico",value=embed("favicon.ico")},
{path="/tytd.svg",value=embed("tytd.svg")},
{path="/tytd-128.png",value=embed("tytd-128.png")},
{path="/tytd-192.png",value=embed("tytd-192.png")},
{path="/tytd-256.png",value=embed("tytd-256.png")},
{path="/tytd-384.png",value=embed("tytd-384.png")},
{path="/tytd-512.png",value=embed("tytd-512.png")},
{path="/tytd-1024.png",value=embed("tytd-1024.png")},
{path="/loading-indicator.svg",value=embed("loading-indicator.svg")},
{path="/theme.css",value=embed("theme.css")},
{path="/video.min.js",value=embed("video.min.js")},
{path="/video-js.css",value=embed("video-js.css")},
{path="/wavy.svg",value=embed("wavy.svg")}
{path="/wavy.svg",value=embed("wavy.svg")},
{path="/site.webmanifest",value=embed("site.webmanifest")},
{path="/offline-progress.html",value=embed("offline-progress.html")},
{path="/offline.html",value=embed("offline.html")},
{path="/offline.js", value=embed("offline.js")},
];
const fileNames = [];
each(var item : TYTDResources)
{
fileNames.Add(item.path);
}
const service_worker_str = embed("service_worker.js").ToString().Replace("[\"<@ASSETS@>\"]",Json.Encode(fileNames)).Replace("<@BUILD_TIME@>",BUILD_TIME.ToString());
var times=1;
class TYTDApp {
@@ -29,21 +50,66 @@ class TYTDApp {
public TYTDApp()
{
Console.WriteLine($"Built at {new DateTime(BUILD_TIME).ToString()}");
var tytdfs = new SubdirFilesystem(FS.Local, GetTYTDDir());
this.TYTD = new TYTD.Downloader(tytdfs,FS.MakeFull(GetTYTDDir()));
this.TYTD.Start();
}
public Handle(ctx)
{
if(ctx.Path == "/service_worker.js" || fileNames.Contains(ctx.Path))
{
const inm=ctx.RequestHeaders.TryGetFirst("If-None-Match");
if(TypeIsString(inm))
{
const strs = inm.Split(", ");
each(var item : strs)
{
if(item == $"W/\"{BUILD_TIME}\"" || item == $"\"{BUILD_TIME}\"")
{
ctx.StatusCode = 304;
ctx.WriteHeaders();
return true;
}
}
}
ctx.WithHeader("ETag",$"W/\"{BUILD_TIME}\"");
}
if(ctx.Path == "/service_worker.js")
{
ctx.WithMimeType(Net.Http.MimeType("service_worker.js")).SendText(service_worker_str);
return true;
}
if(ctx.Path == "/api/v1/auth")
{
if(ctx.Method == "POST")
{
const req=ctx.ReadJson();
const result = this.TYTD.Auth(req.username, req.password);
if(result)
{
ctx.SendJson({
success = true,
flags = result.flags
});
}
else {
ctx.SendJson({
success=false
});
}
return true;
}
return false;
}
if(ctx.Path == "/api/v1/login")
{
if(ctx.Method == "POST")
{
const req=ctx.ReadJson();
const result = this.TYTD.Login(req.username, req.password);
const result = this.TYTD.Login(req.username, req.password, false);
if(result)
{
ctx.SendJson({
@@ -70,14 +136,17 @@ class TYTDApp {
const redirect = ctx.QueryParams.TryGetFirst("redirect") ?? "/";
const username = ctx.QueryParams.TryGetFirst("username") ?? "";
const password = ctx.QueryParams.TryGetFirst("password") ?? "";
if(ctx.Method == "POST")
{
const result = this.TYTD.Login(username, password);
const result = this.TYTD.Login(username, password,true);
if(result)
{
ctx.StatusCode = 303;
ctx.WithHeader("Set-Cookie",$"Session={result}; SameSite=Strict").SendRedirect("/");
var date = new DateTime(DateTime.NowEpoch + UserFlags.Expires);
ctx.WithHeader("Set-Cookie",$"Session={result}; SameSite=Lax; Expires={date.ToHttpDate()}; HttpOnly").SendRedirect(redirect);
return true;
}
else incorrect=true;
@@ -97,6 +166,25 @@ class TYTDApp {
return true;
}
}
if(ctx.Path == "/progress")
{
ctx.WithMimeType("text/html").SendText(
<div>
<h1>You have been logged out</h1>
<a href="./login">Login</a>
</div>
);
return true;
}
if(ctx.Path == "/sso")
{
const app = ctx.QueryParams.TryGetFirst("app") ?? "";
const token = ctx.QueryParams.TryGetFirst("token") ?? "";
const path = $"/sso?app={Net.Http.UrlEncode(app)}&token={Net.Http.UrlEncode(token)}";
ctx.StatusCode = 307;
ctx.SendRedirect($"/login?redirect={Net.Http.UrlEncode(path)}");
return true;
}
ctx.StatusCode = 307;
ctx.SendRedirect($"/login?redirect={Net.Http.UrlEncode(ctx.Path)}");
return true;
@@ -208,7 +296,6 @@ class TYTDApp {
return true;
}
}
if(ctx.Path == "/welcome")
{
const redirect = ctx.QueryParams.TryGetFirst("redirect") ?? "/settings";
@@ -245,11 +332,49 @@ class TYTDApp {
return true;
}
}
if(ctx.Path == "/")
{
ctx.WithMimeType("text/html").SendText(Pages.Index(this.TYTD));
return true;
}
else if(ctx.Path == "/sso")
{
const app = ctx.QueryParams.TryGetFirst("app");
const token = ctx.QueryParams.TryGetFirst("token");
if(TypeIsString(app) && TypeIsString(token))
{
const sso = this.TYTD.GetSSO(app);
if(TypeIsDictionary(sso))
{
const user = this.TYTD.WhoAmI(ctx);
if(TypeIsDictionary(user) && user.flags != 0)
{
const postJson = {
username = user.username,
flags = user.flags,
token = token,
sso_app_key = sso.sso_app_key
};
const resp = Net.Http.MakeRequest(sso.service_auth_post, {
Method = "POST",
Body = Net.Http.TextHttpRequestBody(postJson.ToString(),"application/json")
});
if(resp.StatusCode >= 200 && resp.StatusCode <= 299)
{
ctx.StatusCode = 307;
ctx.SendRedirect($"{sso.service_auth_redirect}{Net.Http.UrlEncode(token)}");
return true;
}
}
}
}
ctx.StatusCode = 401;
return false;
}
else if(ctx.Path == "/passwd")
{
var incorrect=false;
@@ -265,6 +390,7 @@ class TYTDApp {
ctx.WithMimeType("text/html").SendText(Pages.ChangePassword(redirect, incorrect));
}
else if(ctx.Path == "/newuser")
{
var error = null;
@@ -340,6 +466,36 @@ class TYTDApp {
}
return false;
}
else if(ctx.Path == "/api/v1/register_sso")
{
if(ctx.Method!="POST")
{
ctx.WithMimeType("application/json").SendJson({
success=false,
reason = "Method must be post",
type = "method"
});
return true;
}
if(!UserFlags.IsAdmin(this.TYTD.IsLoggedIn(ctx)))
{
ctx.WithMimeType("application/json").SendJson({
success=false,
reason = "You are either not logged in or not admin",
type ="auth"
});
return true;
}
const json = ctx.ReadJson();
const resp = this.TYTD.RegisterSSO(json);
ctx.WithMimeType("application/json").SendJson(
resp
);
return true;
}
else if(ctx.Path == "/api/v1/download")
{
var v = ctx.QueryParams.TryGetFirst("v");
@@ -371,6 +527,20 @@ class TYTDApp {
ctx.WithMimeType("application/json").SendJson(jo);
return true;
}
else if(ctx.Path == "/api/v1/add")
{
if(ctx.Method=="POST")
{
const json = ctx.ReadJson();
each(var item : json)
{
this.TYTD.DownloadItem(item.url,item.res);
}
ctx.StatusCode=204;
ctx.WriteHeaders();
return true;
}
}
else if(ctx.Path == "/api/v1/video.json")
{
var id = ctx.QueryParams.TryGetFirst("v");
@@ -439,6 +609,20 @@ class TYTDApp {
}
return true;
}
else if(ctx.Path == "/api/v1/personal_tmp_link")
{
const name = ctx.QueryParams.TryGetFirst("name");
if(TypeIsString(name))
{
const ents = this.TYTD.GetPersonalListTempUrl(name);
if(TypeIsString(ents))
{
ctx.WithMimeType("text/plain").SendText(ents);
return true;
}
}
}
else if(ctx.Path == "/api/v1/personal")
{
/*
@@ -714,6 +898,36 @@ class TYTDApp {
ctx.WithMimeType("text/html").SendText(Pages.VideoInfo(this.TYTD,ctx));
return true;
}
else if(ctx.Path == "/watch_videos")
{
const video_ids = ctx.QueryParams.TryGetFirst("video_ids");
if(TypeIsString(video_ids))
{
if(ctx.Method=="GET")
{
ctx.WithMimeType("text/html").SendText(Pages.YouTubeAnonyPlaylist(video_ids));
return true;
}
if(ctx.Method=="POST")
{
const name = ctx.QueryParams.TryGetFirst("name");
if(TypeIsString(name))
{
const nameParts=video_ids.Split(",");
Console.WriteLine(nameParts);
each(var item : nameParts)
{
this.TYTD.AddToPersonalList(name,item);
}
Console.WriteLine(name);
ctx.SendRedirect($"/list?name={Net.Http.UrlEncode(name)}",303);
return true;
}
}
}
}
else if(ctx.Path == "/playlist")
{
ctx.WithMimeType("text/html").SendText(Pages.PlaylistInfo(this.TYTD,ctx));
@@ -802,6 +1016,12 @@ class TYTDApp {
ctx.WithMimeType("text/html").SendText(Components.Progress(this.TYTD));
return true;
}
else if(ctx.Path == "/queue-size")
{
ctx.WithMimeType("text/html").SendText(Components.QueueSZ(this.TYTD));
return true;
}
else if(ctx.Path == "/plugins")
{
ctx.WithMimeType("text/html").SendText(Pages.Plugins(this.TYTD,ctx));

View File

@@ -0,0 +1,26 @@
func Pages.YouTubeAnonyPlaylist(video_ids)
{
const html = <null>
<div class="row">
<div class="max"></div>
<div class="min">
<form method="POST" action="./watch_videos">
<input type="hidden" name="video_ids" value={video_ids}>
<div class="row">
<div class="max">
<div class="field label border">
<input type="text" name="name">
<label>Playlist Name</label>
</div>
</div>
<div class="min">
<button>Add</button>
</div>
</div>
</form>
</div>
<div class="max"></div>
</div>
</null>;
return Components.Shell("Create playlist",html ,1);
}