mirror of
https://codeberg.org/ashley/poke
synced 2026-03-03 18:13:45 +00:00
owo
This commit is contained in:
60
core/LightTube/Views/Account/Account.cshtml
Normal file
60
core/LightTube/Views/Account/Account.cshtml
Normal file
@@ -0,0 +1,60 @@
|
||||
@using LightTube.Database
|
||||
@using System.Web
|
||||
@model LightTube.Contexts.BaseContext
|
||||
|
||||
@{
|
||||
ViewBag.Title = "Account";
|
||||
Layout = "_Layout";
|
||||
|
||||
Context.Request.Cookies.TryGetValue("theme", out string theme);
|
||||
if (!new[] { "light", "dark" }.Contains(theme)) theme = "light";
|
||||
|
||||
string newTheme = theme switch {
|
||||
"light" => "dark",
|
||||
"dark" => "light",
|
||||
var _ => "dark"
|
||||
};
|
||||
|
||||
bool compatibility = false;
|
||||
if (Context.Request.Cookies.TryGetValue("compatibility", out string compatibilityString))
|
||||
bool.TryParse(compatibilityString, out compatibility);
|
||||
}
|
||||
|
||||
<div class="login-container">
|
||||
<div>
|
||||
<div class="fullscreen-account-menu">
|
||||
<h1>Settings</h1>
|
||||
<br>
|
||||
<div class="guide-item">
|
||||
<a href="/toggles/theme?redirectUrl=@(HttpUtility.UrlEncode($"{Context.Request.Path}{Context.Request.QueryString}"))">Switch to @(newTheme) theme</a>
|
||||
</div>
|
||||
<br>
|
||||
@if (Context.TryGetUser(out LTUser user, "web"))
|
||||
{
|
||||
<div class="guide-item">
|
||||
<a href="/Account/Settings">Settings</a>
|
||||
</div>
|
||||
@if (user.PasswordHash != "local_account")
|
||||
{
|
||||
<div class="guide-item">
|
||||
<a href="/Account/Logins">Active logins</a>
|
||||
</div>
|
||||
}
|
||||
<div class="guide-item">
|
||||
<a href="/Account/Logout">Log out</a>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="guide-item">
|
||||
<a href="/Account/Login">Log in</a>
|
||||
</div>
|
||||
<div class="guide-item">
|
||||
<a href="/Account/Register">Register</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
</div>
|
||||
</div>
|
||||
52
core/LightTube/Views/Account/AddVideoToPlaylist.cshtml
Normal file
52
core/LightTube/Views/Account/AddVideoToPlaylist.cshtml
Normal file
@@ -0,0 +1,52 @@
|
||||
@using System.Web
|
||||
@using LightTube.Database
|
||||
@model LightTube.Contexts.AddToPlaylistContext
|
||||
|
||||
@{
|
||||
ViewBag.Metadata = new Dictionary<string, string>();
|
||||
ViewBag.Metadata["author"] = Model.Video.Channel.Name;
|
||||
ViewBag.Metadata["og:title"] = Model.Video.Title;
|
||||
ViewBag.Metadata["og:url"] = $"{Url.ActionContext.HttpContext.Request.Scheme}://{Url.ActionContext.HttpContext.Request.Host}{Url.ActionContext.HttpContext.Request.Path}{Url.ActionContext.HttpContext.Request.QueryString}";
|
||||
ViewBag.Metadata["og:image"] = $"{Url.ActionContext.HttpContext.Request.Scheme}://{Url.ActionContext.HttpContext.Request.Host}/proxy/image?url={HttpUtility.UrlEncode(Model.Thumbnail)}";
|
||||
ViewBag.Metadata["twitter:card"] = $"{Url.ActionContext.HttpContext.Request.Scheme}://{Url.ActionContext.HttpContext.Request.Host}/proxy/image?url={HttpUtility.UrlEncode(Model.Thumbnail)}";
|
||||
ViewBag.Title = Model.Video.Title;
|
||||
Layout = "_Layout";
|
||||
}
|
||||
|
||||
<div class="playlist-page">
|
||||
<div class="playlist-info">
|
||||
<div class="thumbnail" style="background-image: url('@Model.Thumbnail')">
|
||||
<a href="/watch?v=@Model.Video.Id">Watch</a>
|
||||
</div>
|
||||
<p class="title">@Model.Video.Title</p>
|
||||
<span class="info">@Model.Video.Views • @Model.Video.UploadDate</span>
|
||||
<div class="channel-info">
|
||||
<a href="/channel/@Model.Video.Channel.Id" class="avatar">
|
||||
<img src="@Model.Video.Channel.Avatars.LastOrDefault()?.Url">
|
||||
</a>
|
||||
<div class="name">
|
||||
<a class="name" href="/channel/@Model.Video.Channel.Id">@Model.Video.Channel.Name</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="video-list playlist-list playlist-video-list">
|
||||
<h3>Add to one of these playlists:</h3>
|
||||
<a class="login-button" href="/Account/CreatePlaylist" style="margin:unset;">Create playlist</a>
|
||||
@foreach (LTPlaylist playlist in Model.Playlists)
|
||||
{
|
||||
<div class="playlist-video">
|
||||
<a href="/playlist?list=@playlist.Id&add=@Model.Id" class="thumbnail"
|
||||
style="background-image: url('https://i.ytimg.com/vi_webp/@playlist.VideoIds.FirstOrDefault()/maxresdefault.webp')">
|
||||
</a>
|
||||
<div class="info">
|
||||
<a href="/playlist?list=@playlist.Id&add=@Model.Id" class="title max-lines-2">
|
||||
@playlist.Name
|
||||
</a>
|
||||
<div>
|
||||
<span>@playlist.VideoIds.Count videos</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
25
core/LightTube/Views/Account/CreatePlaylist.cshtml
Normal file
25
core/LightTube/Views/Account/CreatePlaylist.cshtml
Normal file
@@ -0,0 +1,25 @@
|
||||
@model LightTube.Contexts.BaseContext
|
||||
|
||||
@{
|
||||
ViewBag.Title = "Create Playlist";
|
||||
Layout = "_Layout";
|
||||
}
|
||||
|
||||
<div class="login-container">
|
||||
<div>
|
||||
<div>
|
||||
<form asp-action="CreatePlaylist" method="POST" class="playlist-form">
|
||||
<h1>Create Playlist</h1>
|
||||
<input name="name" type="text" placeholder="Playlist Name">
|
||||
<input name="description" type="text" placeholder="Description">
|
||||
<select name="visibility">
|
||||
<option value="UNLISTED">Anyone with the link can view</option>
|
||||
<option value="PRIVATE">Only you can view</option>
|
||||
</select>
|
||||
<input type="submit" value="Create">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
</div>
|
||||
</div>
|
||||
46
core/LightTube/Views/Account/Delete.cshtml
Normal file
46
core/LightTube/Views/Account/Delete.cshtml
Normal file
@@ -0,0 +1,46 @@
|
||||
@using LightTube.Database
|
||||
@model LightTube.Contexts.MessageContext
|
||||
@{
|
||||
ViewData["Title"] = "Delete Account";
|
||||
Layout = "_Layout";
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(Model.Message))
|
||||
{
|
||||
<div class="login-message">
|
||||
@Model.Message
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="login-container">
|
||||
<div>
|
||||
<div>
|
||||
@if (Context.Request.Cookies.TryGetValue("account_data", out string _))
|
||||
{
|
||||
Context.TryGetUser(out LTUser user, "web");
|
||||
<form asp-action="Delete" method="POST" class="login-form">
|
||||
<h1>Delete Account</h1>
|
||||
<p>Deleting a local account</p>
|
||||
<input name="email" type="hidden" value="@user.UserID">
|
||||
<input name="password" type="hidden" value="@user.PasswordHash">
|
||||
<input type="submit" value="Delete Account" class="login-button danger">
|
||||
</form>
|
||||
}
|
||||
else
|
||||
{
|
||||
<form asp-action="Delete" method="POST" class="login-form">
|
||||
<h1>Delete Account</h1>
|
||||
<input name="userid" type="text" placeholder="UserID">
|
||||
<input name="password" type="password" placeholder="Password">
|
||||
<input type="submit" value="Delete Account" class="login-button danger">
|
||||
</form>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
<h1>Warning!</h1>
|
||||
<p>You cannot undo this operation! After you enter your username and password, your account will get deleted forever.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
29
core/LightTube/Views/Account/Login.cshtml
Normal file
29
core/LightTube/Views/Account/Login.cshtml
Normal file
@@ -0,0 +1,29 @@
|
||||
@model LightTube.Contexts.MessageContext
|
||||
@{
|
||||
ViewData["Title"] = "Login";
|
||||
Layout = "_Layout";
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(Model.Message))
|
||||
{
|
||||
<div class="login-message">
|
||||
@Model.Message
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="login-container">
|
||||
<div>
|
||||
<div>
|
||||
<form asp-action="Login" method="POST" class="login-form">
|
||||
<h1>Log in</h1>
|
||||
<input name="userid" type="text" placeholder="UserID">
|
||||
<input name="password" type="password" placeholder="Password">
|
||||
<input type="submit" value="Login">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Don't have an account?</h2>
|
||||
<a href="/Account/Register" class="login-button">Create an account</a>
|
||||
</div>
|
||||
</div>
|
||||
18
core/LightTube/Views/Account/Logins.cshtml
Normal file
18
core/LightTube/Views/Account/Logins.cshtml
Normal file
@@ -0,0 +1,18 @@
|
||||
@using LightTube.Database
|
||||
@model LightTube.Contexts.LoginsContext
|
||||
@{
|
||||
ViewData["Title"] = "Active Logins";
|
||||
Layout = "_Layout";
|
||||
}
|
||||
|
||||
<h1 style="text-align:center;">Active Logins</h1>
|
||||
<div class="logins-container">
|
||||
@foreach (LTLogin login in Model.Logins)
|
||||
{
|
||||
<div class="login">
|
||||
<h2 class="max-lines-1">@(login.Identifier == Model.CurrentLogin ? "(This window) " : "")@login.GetTitle()</h2>
|
||||
<p>@Html.Raw(login.GetDescription().Replace("\n", "<br>"))</p>
|
||||
<a href="/Account/DisableLogin?id=@login.Identifier" class="login-button" style="color:red;">Disable</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
42
core/LightTube/Views/Account/Register.cshtml
Normal file
42
core/LightTube/Views/Account/Register.cshtml
Normal file
@@ -0,0 +1,42 @@
|
||||
@model LightTube.Contexts.MessageContext
|
||||
@{
|
||||
ViewData["Title"] = "Register";
|
||||
Layout = "_Layout";
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(Model.Message))
|
||||
{
|
||||
<div class="login-message">
|
||||
@Model.Message
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="login-container">
|
||||
<div>
|
||||
<div>
|
||||
<form asp-action="Register" method="POST" class="login-form">
|
||||
<h1>Register</h1>
|
||||
<input name="userid" type="text" placeholder="UserID">
|
||||
<input name="password" type="password" placeholder="Password">
|
||||
<input type="submit" value="Register">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
<h1>...or register with a local account</h1>
|
||||
<h2>What is the difference?</h2>
|
||||
<ul>
|
||||
<li>Remote account data is saved in this lighttube instance, while local account data is stored in
|
||||
your browser's cookies
|
||||
<ul>
|
||||
<li>This means that the author of this lighttube instance cannot see your account data</li>
|
||||
<li>It also means that, if you clear your cookies a lot, your account data will also get
|
||||
lost with the cookies</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<a href="/Account/RegisterLocal" class="login-button">Create local account</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
63
core/LightTube/Views/Account/Settings.cshtml
Normal file
63
core/LightTube/Views/Account/Settings.cshtml
Normal file
@@ -0,0 +1,63 @@
|
||||
@model LightTube.Contexts.SettingsContext
|
||||
|
||||
@{
|
||||
ViewBag.Title = "Settings";
|
||||
Layout = "_Layout";
|
||||
}
|
||||
|
||||
|
||||
<form method="post">
|
||||
<div class="settings-content">
|
||||
<h1 style="text-align:center">Settings</h1>
|
||||
<div>
|
||||
<label for="settings-theme">Theme</label>
|
||||
<select id="settings-theme" name="theme">
|
||||
@Html.Raw($"<option value='light' {(Model.Theme == "light" ? "selected" : "")}>Light</option>")
|
||||
@Html.Raw($"<option value='dark' {(Model.Theme == "dark" ? "selected" : "")}>Dark</option>")
|
||||
</select>
|
||||
<p>This is the visual theme the website will use.</p>
|
||||
</div>
|
||||
<div>
|
||||
<label for="settings-yhl">Content Language</label>
|
||||
<select id="settings-yhl" name="hl">
|
||||
@foreach (KeyValuePair<string, string> o in Model.Languages)
|
||||
{
|
||||
@Html.Raw($"<option value='{o.Key}' {(o.Key == Model.CurrentLanguage ? "selected" : "")}>{o.Value}</option>")
|
||||
}
|
||||
</select>
|
||||
<p>The language YouTube will deliver the content in. This will not affect LightTube's UI language.</p>
|
||||
</div>
|
||||
<div>
|
||||
<label for="settings-ygl">Content Region</label>
|
||||
<select id="settings-ygl" name="gl">
|
||||
@foreach (KeyValuePair<string, string> o in Model.Regions)
|
||||
{
|
||||
@Html.Raw($"<option value='{o.Key}' {(o.Key == Model.CurrentRegion ? "selected" : "")}>{o.Value}</option>")
|
||||
}
|
||||
</select>
|
||||
<p>The language YouTube will deliver the content for. It is used for the explore page and the recommendations.</p>
|
||||
</div>
|
||||
<div>
|
||||
<label for="settings-player">Player</label>
|
||||
<select id="settings-player" name="compatibility">
|
||||
@Html.Raw($"<option value=\"false\" {(Model.CompatibilityMode ? "" : "selected")}>DASH playback with muxed fallback (recommended)</option>")
|
||||
@Html.Raw($"<option value=\"true\" {(Model.CompatibilityMode ? "selected" : "")}>Muxed formats only (only supports 360p & 720p)</option>")
|
||||
</select>
|
||||
<p>Player behaviour. DASH playback allows for resolutions over 720p, but it is not compatible in all browsers. (e.g: Firefox Mobile)</p>
|
||||
</div>
|
||||
<div>
|
||||
<label for="settings-api">API Access</label>
|
||||
<select id="settings-api" name="api-access">
|
||||
@Html.Raw($"<option value=\"true\" {(Model.ApiAccess ? "selected" : "")}>Enabled</option>")
|
||||
@Html.Raw($"<option value=\"false\" {(Model.ApiAccess ? "" : "selected")}>Disabled</option>")
|
||||
</select>
|
||||
<p>This will allow apps to log in using your username and password</p>
|
||||
</div>
|
||||
<div style="display:flex;flex-direction:row">
|
||||
<a href="/Account/Logins" class="login-button">Active Logins</a>
|
||||
<a href="/Account/Delete" class="login-button" style="color:red">Delete Account</a>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
<input type="submit" class="login-button" value="Save"/>
|
||||
</form>
|
||||
26
core/LightTube/Views/Feed/Channels.cshtml
Normal file
26
core/LightTube/Views/Feed/Channels.cshtml
Normal file
@@ -0,0 +1,26 @@
|
||||
@using LightTube.Database
|
||||
@model LightTube.Contexts.FeedContext
|
||||
|
||||
@{
|
||||
ViewBag.Title = "Channel list";
|
||||
}
|
||||
|
||||
<div class="video-list">
|
||||
@foreach (LTChannel channel in Model.Channels)
|
||||
{
|
||||
<div class="channel">
|
||||
<a href="/channel/@channel.ChannelId" class="avatar">
|
||||
<img src="@channel.IconUrl" alt="Channel Avatar">
|
||||
</a>
|
||||
<a href="/channel/@channel.ChannelId" class="info">
|
||||
<span class="name max-lines-2">@channel.Name</span>
|
||||
<div>
|
||||
<div>
|
||||
<span>@channel.Subscribers</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<button class="subscribe-button" data-cid="@channel.ChannelId">Subscribe</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
8
core/LightTube/Views/Feed/Explore.cshtml
Normal file
8
core/LightTube/Views/Feed/Explore.cshtml
Normal file
@@ -0,0 +1,8 @@
|
||||
@{
|
||||
ViewData["Title"] = "Explore";
|
||||
ViewData["SelectedGuideItem"] = "explore";
|
||||
}
|
||||
|
||||
<div style="text-align: center">
|
||||
<h1>Coming soon!</h1>
|
||||
</div>
|
||||
35
core/LightTube/Views/Feed/Playlists.cshtml
Normal file
35
core/LightTube/Views/Feed/Playlists.cshtml
Normal file
@@ -0,0 +1,35 @@
|
||||
@using LightTube.Database
|
||||
@model LightTube.Contexts.PlaylistsContext
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Playlists";
|
||||
ViewData["SelectedGuideItem"] = "library";
|
||||
Layout = "_Layout";
|
||||
}
|
||||
|
||||
<div class="video-list">
|
||||
<h2>Playlists</h2>
|
||||
@foreach (LTPlaylist playlist in Model.Playlists)
|
||||
{
|
||||
<div class="playlist">
|
||||
<a href="/watch?v=@playlist.VideoIds.FirstOrDefault()&list=@playlist.Id" class="thumbnail" style="background-image: url('https://i.ytimg.com/vi_webp/@playlist.VideoIds.FirstOrDefault()/maxresdefault.webp')">
|
||||
<div>
|
||||
<span>@playlist.VideoIds.Count</span><span>VIDEOS</span>
|
||||
</div>
|
||||
</a>
|
||||
<div class="info">
|
||||
<a href="/watch?v=@playlist.VideoIds.FirstOrDefault()&list=@playlist.Id" class="title max-lines-2">@playlist.Name</a>
|
||||
<div>
|
||||
<a href="/channel/@PlaylistManager.GenerateAuthorId(playlist.Author)">@playlist.Author</a>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="/playlist?list=@playlist.Id">
|
||||
<b>View Full Playlist</b>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
55
core/LightTube/Views/Feed/Subscriptions.cshtml
Normal file
55
core/LightTube/Views/Feed/Subscriptions.cshtml
Normal file
@@ -0,0 +1,55 @@
|
||||
@using Humanizer
|
||||
@using LightTube.Database
|
||||
@using System.Web
|
||||
@model LightTube.Contexts.FeedContext
|
||||
@{
|
||||
ViewData["Title"] = "Subscriptions";
|
||||
ViewData["SelectedGuideItem"] = "subs";
|
||||
|
||||
bool minMode = false;
|
||||
if (Context.Request.Cookies.TryGetValue("minMode", out string minModeString))
|
||||
bool.TryParse(minModeString, out minMode);
|
||||
}
|
||||
|
||||
<div class="horizontal-channel-list" style="max-width: @(!Model.MobileLayout ? $"calc(100vw - {(minMode ? 80 : 312)}px);" : "")">
|
||||
<a href="/feed/channels" class="channel">
|
||||
<i class="bi bi-gear"></i>
|
||||
<div class="name max-lines-2">Manage Channels</div>
|
||||
</a>
|
||||
<a href="/rss?token=@HttpUtility.UrlEncode(Model.RssToken)" class="channel">
|
||||
<i class="bi bi-rss"></i>
|
||||
<div class="name max-lines-2">RSS Feed</div>
|
||||
</a>
|
||||
@foreach (LTChannel channel in Model.Channels)
|
||||
{
|
||||
<a href="/channel/@channel.ChannelId" class="channel">
|
||||
<img src="@channel.IconUrl" loading="lazy">
|
||||
<div class="name max-lines-2">@channel.Name</div>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="rich-video-grid">
|
||||
@foreach (FeedVideo video in Model.Videos)
|
||||
{
|
||||
<div class="video">
|
||||
<a href="/watch?v=@video.Id" class="thumbnail img-thumbnail">
|
||||
<img src="@video.Thumbnail" loading="lazy">
|
||||
</a>
|
||||
<a href="/channel/@video.ChannelId" class="avatar">
|
||||
<img src="@Model.Channels.First(x => x.ChannelId == video.ChannelId).IconUrl">
|
||||
</a>
|
||||
<div class="info">
|
||||
<a href="/watch?v=@video.Id" class="title max-lines-2">@video.Title</a>
|
||||
<div>
|
||||
<a href="/channel/@video.ChannelId">@video.ChannelName</a>
|
||||
<div>
|
||||
<span>@video.ViewCount views</span>
|
||||
<span>•</span>
|
||||
<span>@video.PublishedDate.Humanize(DateTimeOffset.Now)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
15
core/LightTube/Views/Home/Index.cshtml
Normal file
15
core/LightTube/Views/Home/Index.cshtml
Normal file
@@ -0,0 +1,15 @@
|
||||
@model LightTube.Contexts.BaseContext
|
||||
@{
|
||||
ViewBag.Metadata = new Dictionary<string, string>
|
||||
{
|
||||
["og:title"] = "LightTube",
|
||||
["og:url"] = $"{Url.ActionContext.HttpContext.Request.Scheme}://{Url.ActionContext.HttpContext.Request.Host}{Url.ActionContext.HttpContext.Request.Path}{Url.ActionContext.HttpContext.Request.QueryString}",
|
||||
["og:description"] = "An alternative, privacy respecting front end for YouTube",
|
||||
};
|
||||
ViewData["Title"] = "Home Page";
|
||||
ViewData["SelectedGuideItem"] = "home";
|
||||
}
|
||||
|
||||
<div style="text-align: center">
|
||||
<h1>@Configuration.Instance.Interface.MessageOfTheDay</h1>
|
||||
</div>
|
||||
17
core/LightTube/Views/Shared/Error.cshtml
Normal file
17
core/LightTube/Views/Shared/Error.cshtml
Normal file
@@ -0,0 +1,17 @@
|
||||
@model LightTube.Contexts.ErrorContext
|
||||
@{
|
||||
ViewData["Title"] = "Error";
|
||||
}
|
||||
|
||||
<h1 class="text-danger">Error.</h1>
|
||||
<h2 class="text-danger">An error occurred while processing your request.</h2>
|
||||
<p>
|
||||
You can try other alternatives to access this resource such as:
|
||||
<b>
|
||||
<a href="https://invidio.us@($"{Model.Path}{Context.Request.QueryString}")">Invidious</a>
|
||||
</b>
|
||||
or
|
||||
<b>
|
||||
<a href="https://youtube.com@($"{Model.Path}{Context.Request.QueryString}")">YouTube</a>
|
||||
</b>
|
||||
</p>
|
||||
139
core/LightTube/Views/Shared/_Layout.cshtml
Normal file
139
core/LightTube/Views/Shared/_Layout.cshtml
Normal file
@@ -0,0 +1,139 @@
|
||||
@using System.Web
|
||||
@using LightTube.Contexts
|
||||
@model LightTube.Contexts.BaseContext
|
||||
@{
|
||||
bool compatibility = false;
|
||||
if (Context.Request.Cookies.TryGetValue("compatibility", out string compatibilityString))
|
||||
bool.TryParse(compatibilityString, out compatibility);
|
||||
|
||||
bool minMode = false;
|
||||
if (Context.Request.Cookies.TryGetValue("minMode", out string minModeString))
|
||||
bool.TryParse(minModeString, out minMode);
|
||||
}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<meta property="og:site_name" content="lighttube" />
|
||||
<meta property="og:type" content="website" />
|
||||
@if (ViewBag.Metadata is not null)
|
||||
{
|
||||
@foreach (KeyValuePair<string, string> metaTag in ViewBag.Metadata)
|
||||
{
|
||||
if (metaTag.Key.StartsWith("og:"))
|
||||
{
|
||||
<meta property="@metaTag.Key" content="@metaTag.Value"/>
|
||||
}
|
||||
else
|
||||
{
|
||||
<meta name="@metaTag.Key" content="@metaTag.Value"/>
|
||||
}
|
||||
}
|
||||
}
|
||||
<meta property="theme-color" content="#AA0000" />
|
||||
<title>@ViewData["Title"] - lighttube</title>
|
||||
@if ((ViewData["HideGuide"] ?? false).Equals(true))
|
||||
{
|
||||
<style> .guide { display: none !important; } </style>
|
||||
}
|
||||
@{
|
||||
Context.Request.Cookies.TryGetValue("theme", out string theme);
|
||||
if (!new[] { "light", "dark" }.Contains(theme)) theme = "light";
|
||||
}
|
||||
<link rel="stylesheet" href="@($"~/css/colors-{theme}.css")" asp-append-version="true"/>
|
||||
@if (Model.MobileLayout)
|
||||
{
|
||||
<link rel="stylesheet" href="~/css/mobile.css" asp-append-version="true"/>
|
||||
<link rel="stylesheet" href="~/css/lt-video/player-mobile.css" asp-append-version="true"/>
|
||||
}
|
||||
else
|
||||
{
|
||||
<link rel="stylesheet" href="~/css/desktop.css" asp-append-version="true"/>
|
||||
<link rel="stylesheet" href="~/css/lt-video/player-desktop.css" asp-append-version="true"/>
|
||||
}
|
||||
<link rel="stylesheet" href="~/css/bootstrap-icons/bootstrap-icons.css" asp-append-version="true"/>
|
||||
<link rel="icon" href="~/favicon.ico">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="top-bar @(ViewData["UseFullSizeSearchBar"]?.Equals(true) ?? false ? "full-size-search" : "")">
|
||||
<a class="logo" href="/">light<b>tube</b></a>
|
||||
<div class="divider"></div>
|
||||
<form action="/results">
|
||||
<input type="text" placeholder="Search" name="search_query" value="@(Model is SearchContext ctx ? ctx.Query : Context.Request.Cookies.TryGetValue("search_query", out string s) ? s : "")">
|
||||
<input type="submit" value="Search">
|
||||
</form>
|
||||
<div class="divider"></div>
|
||||
<div class="search-button">
|
||||
<a class="icon-link" href="/results">
|
||||
<i class="bi bi-search"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="account" tabindex="-1">
|
||||
<a class="icon-link" href="/Account">
|
||||
<i class="bi bi-person-circle"></i>
|
||||
</a>
|
||||
<div class="account-menu">
|
||||
@Html.Partial("_LoginLogoutPartial")
|
||||
<div class="guide-item"><a href="/toggles/theme?redirectUrl=@(HttpUtility.UrlEncode($"{Context.Request.Path}{Context.Request.QueryString}"))">Toggle Theme</a></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="guide @(minMode ? "minmode" : "")">
|
||||
<div class="guide-item @(ViewData["SelectedGuideItem"] as string == "home" ? "active" : "")">
|
||||
<a href="/">
|
||||
<i class="icon bi bi-house-door"></i>
|
||||
Home
|
||||
</a>
|
||||
</div>
|
||||
<div class="guide-item @(ViewData["SelectedGuideItem"] as string == "explore" ? "active" : "")">
|
||||
<a href="/feed/explore">
|
||||
<i class="icon bi bi-compass"></i>
|
||||
Explore
|
||||
</a>
|
||||
</div>
|
||||
<div class="guide-item @(ViewData["SelectedGuideItem"] as string == "subs" ? "active" : "")">
|
||||
<a href="/feed/subscriptions">
|
||||
<i class="icon bi bi-inboxes"></i>
|
||||
Subscriptions
|
||||
</a>
|
||||
</div>
|
||||
<div class="guide-item @(ViewData["SelectedGuideItem"] as string == "library" ? "active" : "")">
|
||||
<a href="/feed/library">
|
||||
<i class="icon bi bi-list-ul"></i>
|
||||
Library
|
||||
</a>
|
||||
</div>
|
||||
<div class="hide-on-minmode guide-item">
|
||||
<a href="/toggles/collapse_guide?redirectUrl=@(HttpUtility.UrlEncode($"{Context.Request.Path}{Context.Request.QueryString}"))">
|
||||
<i class="icon"><i class="bi bi-arrow-left-square"></i></i>
|
||||
Collapse Guide
|
||||
</a>
|
||||
</div>
|
||||
<div class="show-on-minmode guide-item">
|
||||
<a href="/toggles/collapse_guide?redirectUrl=@(HttpUtility.UrlEncode($"{Context.Request.Path}{Context.Request.QueryString}"))">
|
||||
<i class="icon"><i class="bi bi-arrow-right-square"></i></i>
|
||||
Expand
|
||||
</a>
|
||||
</div>
|
||||
<hr class="hide-on-minmode">
|
||||
<p class="hide-on-minmode">
|
||||
<a href="https://gitlab.com/kuylar/lighttube/-/blob/master/README.md">About</a><br>
|
||||
<a href="https://gitlab.com/kuylar/lighttube/-/blob/master/OTHERLIBS.md">How LightTube works</a><br>
|
||||
<a href="https://gitlab.com/kuylar/lighttube">Source code</a>
|
||||
<a href="https://gitlab.com/kuylar/lighttube/-/wikis/XML-API">API</a>
|
||||
<a href="https://gitlab.com/kuylar/lighttube/-/blob/master/LICENSE">License</a><br>
|
||||
<span style="font-weight: normal">Running on LightTube v@(Utils.GetVersion())</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="app">
|
||||
@RenderBody()
|
||||
</div>
|
||||
|
||||
<script src="~/js/site.js" asp-append-version="true"></script>
|
||||
@await RenderSectionAsync("Scripts", required: false)
|
||||
</body>
|
||||
</html>
|
||||
16
core/LightTube/Views/Shared/_LoginLogoutPartial.cshtml
Normal file
16
core/LightTube/Views/Shared/_LoginLogoutPartial.cshtml
Normal file
@@ -0,0 +1,16 @@
|
||||
@using LightTube.Database
|
||||
@if (Context.TryGetUser(out LTUser user, "web"))
|
||||
{
|
||||
<div class="guide-item"><a>@user.UserID.Split("@")[0]</a></div>
|
||||
@if (user.PasswordHash != "local_account")
|
||||
{
|
||||
<div class="guide-item"><a href="/Account/Logins">Active logins</a></div>
|
||||
}
|
||||
<div class="guide-item"><a href="/Account/Logout">Log out</a></div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="guide-item"><a href="/Account/Login">Log in</a></div>
|
||||
<div class="guide-item"><a href="/Account/Register">Register</a></div>
|
||||
}
|
||||
<div class="guide-item"><a href="/Account/Settings">Settings</a></div>
|
||||
80
core/LightTube/Views/Youtube/Channel.cshtml
Normal file
80
core/LightTube/Views/Youtube/Channel.cshtml
Normal file
@@ -0,0 +1,80 @@
|
||||
@using InnerTube.Models
|
||||
@using System.Web
|
||||
@model LightTube.Contexts.ChannelContext
|
||||
|
||||
@{
|
||||
ViewBag.Metadata = new Dictionary<string, string>();
|
||||
ViewBag.Metadata["og:title"] = Model.Channel.Name;
|
||||
ViewBag.Metadata["og:url"] = $"{Url.ActionContext.HttpContext.Request.Scheme}://{Url.ActionContext.HttpContext.Request.Host}{Url.ActionContext.HttpContext.Request.Path}{Url.ActionContext.HttpContext.Request.QueryString}";
|
||||
ViewBag.Metadata["og:image"] = $"{Url.ActionContext.HttpContext.Request.Scheme}://{Url.ActionContext.HttpContext.Request.Host}/proxy/image?url={HttpUtility.UrlEncode(Model.Channel.Avatars.FirstOrDefault()?.Url?.ToString())}";
|
||||
ViewBag.Metadata["twitter:card"] = $"{Url.ActionContext.HttpContext.Request.Scheme}://{Url.ActionContext.HttpContext.Request.Host}/proxy/image?url={HttpUtility.UrlEncode(Model.Channel.Avatars.LastOrDefault()?.Url?.ToString())}";
|
||||
ViewBag.Metadata["og:description"] = Model.Channel.Description;
|
||||
ViewBag.Title = Model.Channel.Name;
|
||||
Layout = "_Layout";
|
||||
|
||||
DynamicItem[] contents;
|
||||
try
|
||||
{
|
||||
contents = ((ItemSectionItem)((ItemSectionItem)Model.Channel.Videos[0]).Contents[0]).Contents;
|
||||
}
|
||||
catch
|
||||
{
|
||||
contents = Model.Channel.Videos;
|
||||
}
|
||||
}
|
||||
|
||||
<div class="channel-page">
|
||||
@if (Model.Channel.Banners.Length > 0)
|
||||
{
|
||||
<img class="channel-banner" alt="Channel Banner" src="@Model.Channel.Banners.Last().Url">
|
||||
}
|
||||
|
||||
<div class="channel-info-container">
|
||||
<div class="channel-info">
|
||||
<a href="/channel/@Model.Channel.Id" class="avatar">
|
||||
<img src="@Model.Channel.Avatars.LastOrDefault()?.Url" alt="Channel Avatar">
|
||||
</a>
|
||||
<div class="name">
|
||||
<a>@Model.Channel.Name</a>
|
||||
<span>@Model.Channel.Subscribers</span>
|
||||
</div>
|
||||
<button class="subscribe-button" data-cid="@Model.Channel.Id">Subscribe</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>About</h3>
|
||||
<p>@Html.Raw(Model.Channel.GetHtmlDescription())</p>
|
||||
<br><br>
|
||||
<h3>Uploads</h3>
|
||||
<div class="video-grid">
|
||||
@foreach (VideoItem video in contents.Where(x => x is VideoItem).Cast<VideoItem>())
|
||||
{
|
||||
<a href="/watch?v=@video.Id" class="video">
|
||||
<div class="thumbnail" style="background-image: url('@video.Thumbnails.LastOrDefault()?.Url')"><span class="video-length">@video.Duration</span></div>
|
||||
<div class="info">
|
||||
<span class="title max-lines-2">@video.Title</span>
|
||||
<div>
|
||||
<div>
|
||||
<span>@video.Views views</span>
|
||||
<span>@video.UploadedAt</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="pagination-buttons">
|
||||
@if (!string.IsNullOrWhiteSpace(Model.ContinuationToken))
|
||||
{
|
||||
<a href="/channel?id=@Model.Id">First Page</a>
|
||||
}
|
||||
<div class="divider"></div>
|
||||
<span>•</span>
|
||||
<div class="divider"></div>
|
||||
@if (!string.IsNullOrWhiteSpace(contents.FirstOrDefault(x => x is ContinuationItem)?.Id))
|
||||
{
|
||||
<a href="/channel/@Model.Id?continuation=@(contents.FirstOrDefault(x => x is ContinuationItem)?.Id)">Next Page</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
95
core/LightTube/Views/Youtube/Download.cshtml
Normal file
95
core/LightTube/Views/Youtube/Download.cshtml
Normal file
@@ -0,0 +1,95 @@
|
||||
@using System.Web
|
||||
@using InnerTube
|
||||
@using InnerTube.Models
|
||||
@model LightTube.Contexts.PlayerContext
|
||||
|
||||
@{
|
||||
ViewBag.Metadata = new Dictionary<string, string>();
|
||||
ViewBag.Metadata["author"] = Model.Video.Channel.Name;
|
||||
ViewBag.Metadata["og:title"] = Model.Player.Title;
|
||||
ViewBag.Metadata["og:url"] = $"{Url.ActionContext.HttpContext.Request.Scheme}://{Url.ActionContext.HttpContext.Request.Host}{Url.ActionContext.HttpContext.Request.Path}{Url.ActionContext.HttpContext.Request.QueryString}";
|
||||
ViewBag.Metadata["og:image"] = $"{Url.ActionContext.HttpContext.Request.Scheme}://{Url.ActionContext.HttpContext.Request.Host}/proxy/image?url={HttpUtility.UrlEncode(Model.Player.Thumbnails.FirstOrDefault()?.Url?.ToString())}";
|
||||
ViewBag.Metadata["twitter:card"] = $"{Url.ActionContext.HttpContext.Request.Scheme}://{Url.ActionContext.HttpContext.Request.Host}/proxy/image?url={HttpUtility.UrlEncode(Model.Player.Thumbnails.LastOrDefault()?.Url?.ToString())}";
|
||||
ViewBag.Title = Model.Player.Title;
|
||||
Layout = "_Layout";
|
||||
}
|
||||
|
||||
<div class="playlist-page">
|
||||
<div class="playlist-info">
|
||||
<div class="thumbnail" style="background-image: url('@Model.Player.Thumbnails.Last().Url')">
|
||||
<a href="/watch?v=@Model.Player.Id">Watch</a>
|
||||
</div>
|
||||
<p class="title">@Model.Player.Title</p>
|
||||
<span class="info">@Model.Video.Views • @Model.Video.UploadDate</span>
|
||||
<div class="channel-info">
|
||||
<a href="/channel/@Model.Player.Channel.Id" class="avatar">
|
||||
<img src="@Model.Player.Channel.Avatars.LastOrDefault()?.Url">
|
||||
</a>
|
||||
<div class="name">
|
||||
<a class="name" href="/channel/@Model.Player.Channel.Id">@Model.Player.Channel.Name</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="video-list download-list playlist-video-list">
|
||||
<div class="format-list">
|
||||
<h2>Muxed formats</h2>
|
||||
<p>These downloads have both video and audio in them</p>
|
||||
@foreach (Format format in Model.Player.Formats)
|
||||
{
|
||||
<div class="download-format">
|
||||
<div>
|
||||
@format.FormatNote
|
||||
</div>
|
||||
<a href="/proxy/download/@Model.Video.Id/@format.FormatId/@(HttpUtility.UrlEncode(Model.Video.Title)).@format.GetExtension()">
|
||||
<i class="bi bi-download"></i>
|
||||
Download through LightTube
|
||||
</a>
|
||||
<a href="@format.Url">
|
||||
<i class="bi bi-cloud-download"></i>
|
||||
Download through YouTube
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="format-list">
|
||||
<h2>Audio only formats</h2>
|
||||
<p>These downloads have only have audio in them</p>
|
||||
@foreach (Format format in Model.Player.AdaptiveFormats.Where(x => x.VideoCodec == "none"))
|
||||
{
|
||||
<div class="download-format">
|
||||
<div>
|
||||
@format.FormatNote (Codec: @format.AudioCodec, Sample Rate: @format.AudioSampleRate)
|
||||
</div>
|
||||
<a href="/proxy/download/@Model.Video.Id/@format.FormatId/@(HttpUtility.UrlEncode(Model.Video.Title)).@format.GetExtension()">
|
||||
<i class="bi bi-download"></i>
|
||||
Download through LightTube
|
||||
</a>
|
||||
<a href="@format.Url">
|
||||
<i class="bi bi-cloud-download"></i>
|
||||
Download through YouTube
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="format-list">
|
||||
<h2>Video only formats</h2>
|
||||
<p>These downloads have only have video in them</p>
|
||||
@foreach (Format format in Model.Player.AdaptiveFormats.Where(x => x.AudioCodec == "none"))
|
||||
{
|
||||
<div class="download-format">
|
||||
<div>
|
||||
@format.FormatNote (Codec: @format.VideoCodec)
|
||||
</div>
|
||||
<a href="/proxy/download/@Model.Video.Id/@format.FormatId/@(HttpUtility.UrlEncode(Model.Video.Title)).@format.GetExtension()">
|
||||
<i class="bi bi-download"></i>
|
||||
Download through LightTube
|
||||
</a>
|
||||
<a href="@format.Url">
|
||||
<i class="bi bi-cloud-download"></i>
|
||||
Download through YouTube
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
146
core/LightTube/Views/Youtube/Embed.cshtml
Normal file
146
core/LightTube/Views/Youtube/Embed.cshtml
Normal file
@@ -0,0 +1,146 @@
|
||||
@using System.Collections.Specialized
|
||||
@using System.Web
|
||||
@using InnerTube.Models
|
||||
@model LightTube.Contexts.PlayerContext
|
||||
|
||||
@{
|
||||
ViewBag.Metadata = new Dictionary<string, string>();
|
||||
ViewBag.Metadata["author"] = Model.Video.Channel.Name;
|
||||
ViewBag.Metadata["og:title"] = Model.Player.Title;
|
||||
ViewBag.Metadata["og:url"] = $"{Url.ActionContext.HttpContext.Request.Scheme}://{Url.ActionContext.HttpContext.Request.Host}{Url.ActionContext.HttpContext.Request.Path}{Url.ActionContext.HttpContext.Request.QueryString}";
|
||||
ViewBag.Metadata["og:image"] = $"{Url.ActionContext.HttpContext.Request.Scheme}://{Url.ActionContext.HttpContext.Request.Host}/proxy/image?url={HttpUtility.UrlEncode(Model.Player.Thumbnails.FirstOrDefault()?.Url?.ToString())}";
|
||||
ViewBag.Metadata["twitter:card"] = $"{Url.ActionContext.HttpContext.Request.Scheme}://{Url.ActionContext.HttpContext.Request.Host}/proxy/image?url={HttpUtility.UrlEncode(Model.Player.Thumbnails.LastOrDefault()?.Url?.ToString())}";
|
||||
ViewBag.Metadata["og:description"] = Model.Player.Description;
|
||||
ViewBag.Title = Model.Player.Title;
|
||||
|
||||
Layout = null;
|
||||
try
|
||||
{
|
||||
ViewBag.Metadata["og:video"] = $"/proxy/video?url={HttpUtility.UrlEncode(Model.Player.Formats.First().Url.ToString())}";
|
||||
Model.Resolution ??= Model.Player.Formats.First().FormatNote;
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
bool live = Model.Player.Formats.Length == 0 && Model.Player.AdaptiveFormats.Length > 0;
|
||||
bool canPlay = true;
|
||||
}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<meta property="og:site_name" content="lighttube"/>
|
||||
<meta property="og:type" content="website"/>
|
||||
@if (ViewBag.Metadata is not null)
|
||||
{
|
||||
@foreach (KeyValuePair<string, string> metaTag in ViewBag.Metadata)
|
||||
{
|
||||
if (metaTag.Key.StartsWith("og:"))
|
||||
{
|
||||
<meta property="@metaTag.Key" content="@metaTag.Value"/>
|
||||
}
|
||||
else
|
||||
{
|
||||
<meta name="@metaTag.Key" content="@metaTag.Value"/>
|
||||
}
|
||||
}
|
||||
}
|
||||
<meta property="theme-color" content="#AA0000"/>
|
||||
<title>@ViewData["Title"] - lighttube</title>
|
||||
<link rel="stylesheet" href="~/css/bootstrap-icons/bootstrap-icons.css"/>
|
||||
<link rel="stylesheet" href="~/css/desktop.css" asp-append-version="true"/>
|
||||
<link rel="stylesheet" href="~/css/lt-video/player-desktop.css" asp-append-version="true"/>
|
||||
<link rel="icon" href="~/favicon.ico">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@if (live)
|
||||
{
|
||||
<video class="player" poster="@Model.Player.Thumbnails.LastOrDefault()?.Url">
|
||||
</video>
|
||||
}
|
||||
else if (Model.Player.Formats.Length > 0)
|
||||
{
|
||||
<video class="player" controls src="/proxy/media/@Model.Player.Id/@HttpUtility.UrlEncode(Model.Player.Formats.First(x => x.FormatNote == Model.Resolution && x.FormatId != "17").FormatId)" poster="@Model.Player.Thumbnails.LastOrDefault()?.Url">
|
||||
@foreach (Subtitle subtitle in Model.Player.Subtitles ?? Array.Empty<Subtitle>())
|
||||
{
|
||||
@:<track src="/proxy/caption/@Model.Player.Id/@HttpUtility.UrlEncode(subtitle.Language).Replace("+", "%20")" label="@subtitle.Language" kind="subtitles">
|
||||
}
|
||||
</video>
|
||||
}
|
||||
else
|
||||
{
|
||||
canPlay = false;
|
||||
<div id="player" class="player error" style="background-image: url('@Model.Player.Thumbnails.LastOrDefault()?.Url')">
|
||||
@if (string.IsNullOrWhiteSpace(Model.Player.ErrorMessage))
|
||||
{
|
||||
<span>
|
||||
No playable streams returned from the API (@Model.Player.Formats.Length/@Model.Player.AdaptiveFormats.Length)
|
||||
</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>
|
||||
@Model.Player.ErrorMessage
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (canPlay)
|
||||
{
|
||||
<script src="/js/lt-video/player-desktop.js"></script>
|
||||
@if (!Model.CompatibilityMode && !live)
|
||||
{
|
||||
<script src="/js/shaka-player/shaka-player.compiled.min.js"></script>
|
||||
<script>
|
||||
let player = undefined;
|
||||
loadPlayerWithShaka("video", {
|
||||
"id": "@Model.Video.Id",
|
||||
"title": "@Html.Raw(Model.Video.Title.Replace("\"", "\\\""))",
|
||||
"embed": true,
|
||||
"live": false,
|
||||
"storyboard": "/proxy/image?url=@HttpUtility.UrlEncode(Model.Player.Storyboards.FirstOrDefault())"
|
||||
}, [
|
||||
@foreach(Format f in Model.Player.Formats.Reverse())
|
||||
{
|
||||
@:{"height": @f.Resolution.Split("x")[1],"label":"@f.FormatName","src": "/proxy/video?url=@HttpUtility.UrlEncode(f.Url)"},
|
||||
}
|
||||
], "https://@(Context.Request.Host)/manifest/@(Model.Video.Id).mpd").then(x => player = x).catch(alert);;
|
||||
</script>
|
||||
}
|
||||
else if (live)
|
||||
{
|
||||
<script src="/js/hls.js/hls.min.js"></script>
|
||||
<script>
|
||||
let player = undefined;
|
||||
loadPlayerWithHls("video", {
|
||||
"id": "@(Model.Video.Id)",
|
||||
"title": "@Html.Raw(Model.Video.Title.Replace("\"", "\\\""))",
|
||||
"embed": true,
|
||||
"live": true
|
||||
}, "https://@(Context.Request.Host)/manifest/@(Model.Video.Id).m3u8").then(x => player = x).catch(alert);
|
||||
</script>
|
||||
}
|
||||
else
|
||||
{
|
||||
<script>
|
||||
const player = new Player("video", {
|
||||
"id": "@Model.Video.Id",
|
||||
"title": "@Html.Raw(Model.Video.Title.Replace("\"", "\\\""))",
|
||||
"embed": true,
|
||||
"live": false,
|
||||
"storyboard": "/proxy/image?url=@HttpUtility.UrlEncode(Model.Player.Storyboards.FirstOrDefault())"
|
||||
}, [
|
||||
@foreach(Format f in Model.Player.Formats.Reverse())
|
||||
{
|
||||
@:{"height": @f.Resolution.Split("x")[1],"label":"@f.FormatName","src": "/proxy/video?url=@HttpUtility.UrlEncode(f.Url)"},
|
||||
}
|
||||
]);
|
||||
</script>
|
||||
}
|
||||
}
|
||||
</body>
|
||||
</html>
|
||||
85
core/LightTube/Views/Youtube/Playlist.cshtml
Normal file
85
core/LightTube/Views/Youtube/Playlist.cshtml
Normal file
@@ -0,0 +1,85 @@
|
||||
@using InnerTube.Models
|
||||
@using System.Web
|
||||
@model LightTube.Contexts.PlaylistContext
|
||||
|
||||
@{
|
||||
ViewBag.Title = Model.Playlist.Title;
|
||||
ViewBag.Metadata = new Dictionary<string, string>();
|
||||
ViewBag.Metadata["og:title"] = Model.Playlist.Title;
|
||||
ViewBag.Metadata["og:url"] = $"{Url.ActionContext.HttpContext.Request.Scheme}://{Url.ActionContext.HttpContext.Request.Host}{Url.ActionContext.HttpContext.Request.Path}{Url.ActionContext.HttpContext.Request.QueryString}";
|
||||
ViewBag.Metadata["og:image"] = $"{Url.ActionContext.HttpContext.Request.Scheme}://{Url.ActionContext.HttpContext.Request.Host}/proxy/image?url={HttpUtility.UrlEncode(Model.Playlist.Thumbnail.FirstOrDefault()?.Url?.ToString())}";
|
||||
ViewBag.Metadata["twitter:card"] = $"{Url.ActionContext.HttpContext.Request.Scheme}://{Url.ActionContext.HttpContext.Request.Host}/proxy/image?url={HttpUtility.UrlEncode(Model.Playlist.Thumbnail.LastOrDefault()?.Url?.ToString())}";
|
||||
ViewBag.Metadata["og:description"] = Model.Playlist.Description;
|
||||
Layout = "_Layout";
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(Model.Message))
|
||||
{
|
||||
<div class="playlist-message" style="padding: 16px;background-color: var(--border-color); color: var(--text-primary);">
|
||||
@Model.Message
|
||||
</div>
|
||||
}
|
||||
<div class="playlist-page">
|
||||
<div class="playlist-info">
|
||||
<div class="thumbnail" style="background-image: url('@Model.Playlist.Thumbnail.LastOrDefault()?.Url')">
|
||||
<a href="/watch?v=@Model.Playlist.Videos.FirstOrDefault()?.Id&list=@Model.Id">Play all</a>
|
||||
</div>
|
||||
<p class="title">@Model.Playlist.Title</p>
|
||||
<span class="info">@Model.Playlist.VideoCount videos • @Model.Playlist.ViewCount views • @Model.Playlist.LastUpdated</span>
|
||||
<span class="description">@Html.Raw(Model.Playlist.GetHtmlDescription())</span>
|
||||
<a href="/playlist?list=@Model.Id&remove=true" class="login-button" style="margin:unset;">
|
||||
<i class="bi bi-trash"></i>
|
||||
Delete playlist
|
||||
</a>
|
||||
<div class="channel-info">
|
||||
<a href="/channel/@Model.Playlist.Channel.Id" class="avatar">
|
||||
<img src="@Model.Playlist.Channel.Avatars.LastOrDefault()?.Url">
|
||||
</a>
|
||||
<div class="name">
|
||||
<a class="name" href="/channel/@Model.Playlist.Channel.Id">@Model.Playlist.Channel.Name</a>
|
||||
</div>
|
||||
<button class="subscribe-button" data-cid="@Model.Playlist.Channel.Id">Subscribe</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="video-list playlist-video-list">
|
||||
@foreach (PlaylistVideoItem video in Model.Playlist.Videos.Cast<PlaylistVideoItem>())
|
||||
{
|
||||
<div class="playlist-video">
|
||||
<a href="/watch?v=@video.Id&list=@Model.Id" class="index">
|
||||
@video.Index
|
||||
</a>
|
||||
<a href="/watch?v=@video.Id&list=@Model.Id" class="thumbnail"
|
||||
style="background-image: url('@video.Thumbnails.LastOrDefault()?.Url')">
|
||||
<span class="video-length">@video.Duration</span>
|
||||
</a>
|
||||
<div class="info">
|
||||
<a href="/watch?v=@video.Id&list=@Model.Id" class="title max-lines-2">
|
||||
@video.Title
|
||||
</a>
|
||||
<div>
|
||||
<a href="/channel/@video.Channel.Name">@video.Channel.Name</a>
|
||||
</div>
|
||||
</div>
|
||||
@if (Model.Editable)
|
||||
{
|
||||
<a href="/playlist?list=@Model.Id&delete=@(video.Index - 1)" class="edit">
|
||||
<i class="bi bi-trash"></i>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="pagination-buttons">
|
||||
@if (!string.IsNullOrWhiteSpace(Model.ContinuationToken))
|
||||
{
|
||||
<a href="/playlist?list=@Model.Id">First Page</a>
|
||||
}
|
||||
<div class="divider"></div>
|
||||
<span>•</span>
|
||||
<div class="divider"></div>
|
||||
@if (!string.IsNullOrWhiteSpace(Model.Playlist.ContinuationKey))
|
||||
{
|
||||
<a href="/playlist?list=@Model.Id&continuation=@Model.Playlist.ContinuationKey">Next Page</a>
|
||||
}
|
||||
</div>
|
||||
28
core/LightTube/Views/Youtube/Search.cshtml
Normal file
28
core/LightTube/Views/Youtube/Search.cshtml
Normal file
@@ -0,0 +1,28 @@
|
||||
@using InnerTube.Models
|
||||
@model LightTube.Contexts.SearchContext
|
||||
|
||||
@{
|
||||
ViewBag.Title = Model.Query;
|
||||
Layout = "_Layout";
|
||||
ViewData["UseFullSizeSearchBar"] = Model.MobileLayout;
|
||||
}
|
||||
|
||||
<div class="video-list">
|
||||
@foreach (DynamicItem preview in Model.Results.Results)
|
||||
{
|
||||
@preview.GetHtml()
|
||||
}
|
||||
</div>
|
||||
<div class="pagination-buttons">
|
||||
@if (!string.IsNullOrWhiteSpace(Model.ContinuationKey))
|
||||
{
|
||||
<a href="/results?search_query=@Model.Query">First Page</a>
|
||||
}
|
||||
<div class="divider"></div>
|
||||
<span>•</span>
|
||||
<div class="divider"></div>
|
||||
@if (!string.IsNullOrWhiteSpace(Model.Results.ContinuationKey))
|
||||
{
|
||||
<a href="/results?search_query=@Model.Query&continuation=@Model.Results.ContinuationKey">Next Page</a>
|
||||
}
|
||||
</div>
|
||||
325
core/LightTube/Views/Youtube/Watch.cshtml
Normal file
325
core/LightTube/Views/Youtube/Watch.cshtml
Normal file
@@ -0,0 +1,325 @@
|
||||
@using System.Text.RegularExpressions
|
||||
@using System.Web
|
||||
@using InnerTube.Models
|
||||
@model LightTube.Contexts.PlayerContext
|
||||
|
||||
@{
|
||||
bool compatibility = false;
|
||||
if (Context.Request.Cookies.TryGetValue("compatibility", out string compatibilityString))
|
||||
bool.TryParse(compatibilityString, out compatibility);
|
||||
|
||||
ViewBag.Metadata = new Dictionary<string, string>();
|
||||
ViewBag.Metadata["author"] = Model.Video.Channel.Name;
|
||||
ViewBag.Metadata["og:title"] = Model.Player.Title;
|
||||
ViewBag.Metadata["og:url"] = $"{Url.ActionContext.HttpContext.Request.Scheme}://{Url.ActionContext.HttpContext.Request.Host}{Url.ActionContext.HttpContext.Request.Path}{Url.ActionContext.HttpContext.Request.QueryString}";
|
||||
ViewBag.Metadata["og:image"] = $"{Url.ActionContext.HttpContext.Request.Scheme}://{Url.ActionContext.HttpContext.Request.Host}/proxy/image?url={HttpUtility.UrlEncode(Model.Player.Thumbnails.FirstOrDefault()?.Url?.ToString())}";
|
||||
ViewBag.Metadata["twitter:card"] = $"{Url.ActionContext.HttpContext.Request.Scheme}://{Url.ActionContext.HttpContext.Request.Host}/proxy/image?url={HttpUtility.UrlEncode(Model.Player.Thumbnails.LastOrDefault()?.Url?.ToString())}";
|
||||
ViewBag.Metadata["og:description"] = Model.Player.Description;
|
||||
ViewBag.Title = Model.Player.Title;
|
||||
|
||||
Layout = "_Layout";
|
||||
try
|
||||
{
|
||||
ViewBag.Metadata["og:video"] = $"/proxy/video?url={HttpUtility.UrlEncode(Model.Player.Formats.First().Url.ToString())}";
|
||||
Model.Resolution ??= Model.Player.Formats.First().FormatNote;
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
ViewData["HideGuide"] = true;
|
||||
|
||||
bool live = Model.Player.Formats.Length == 0 && Model.Player.AdaptiveFormats.Length > 0;
|
||||
string description = Model.Video.GetHtmlDescription();
|
||||
const string youtubePattern = @"[w.]*youtube[-nockie]*\.com";
|
||||
|
||||
// turn URLs into hyperlinks
|
||||
Regex urlRegex = new(youtubePattern, RegexOptions.IgnoreCase);
|
||||
Match m;
|
||||
for (m = urlRegex.Match(description); m.Success; m = m.NextMatch())
|
||||
description = description.Replace(m.Groups[0].ToString(),
|
||||
$"{Url.ActionContext.HttpContext.Request.Host}");
|
||||
|
||||
bool canPlay = true;
|
||||
}
|
||||
|
||||
<!-- TODO: chapters -->
|
||||
<div class="watch-page">
|
||||
<div class="primary">
|
||||
<div class="video-player-container">
|
||||
|
||||
@if (live)
|
||||
{
|
||||
<video class="player" poster="@Model.Player.Thumbnails.LastOrDefault()?.Url">
|
||||
</video>
|
||||
}
|
||||
else if (Model.Player.Formats.Length > 0)
|
||||
{
|
||||
<video class="player" controls src="/proxy/media/@Model.Player.Id/@HttpUtility.UrlEncode(Model.Player.Formats.First(x => x.FormatNote == Model.Resolution && x.FormatId != "17").FormatId)" poster="@Model.Player.Thumbnails.LastOrDefault()?.Url">
|
||||
@foreach (Subtitle subtitle in Model.Player.Subtitles ?? Array.Empty<Subtitle>())
|
||||
{
|
||||
@:<track src="/proxy/caption/@Model.Player.Id/@HttpUtility.UrlEncode(subtitle.Language).Replace("+", "%20")" label="@subtitle.Language" kind="subtitles">
|
||||
}
|
||||
</video>
|
||||
}
|
||||
else
|
||||
{
|
||||
canPlay = false;
|
||||
<div id="player" class="player error" style="background-image: url('@Model.Player.Thumbnails.LastOrDefault()?.Url')">
|
||||
@if (string.IsNullOrWhiteSpace(Model.Player.ErrorMessage))
|
||||
{
|
||||
<span>
|
||||
No playable streams returned from the API (@Model.Player.Formats.Length/@Model.Player.AdaptiveFormats.Length)
|
||||
</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>
|
||||
@Model.Player.ErrorMessage
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@if (Model.MobileLayout)
|
||||
{
|
||||
<div class="video-info">
|
||||
<div class="video-title">@Model.Video.Title</div>
|
||||
<div class="video-info-bar">
|
||||
<span>@Model.Video.Views</span>
|
||||
<span>Published @Model.Video.UploadDate</span>
|
||||
<div class="divider"></div>
|
||||
<div class="video-info-buttons">
|
||||
<div>
|
||||
<i class="bi bi-hand-thumbs-up"></i><span>@Model.Engagement.Likes</span>
|
||||
</div>
|
||||
<div>
|
||||
<i class="bi bi-hand-thumbs-down"></i><span>@Model.Engagement.Dislikes</span>
|
||||
</div>
|
||||
<a href="/download?v=@Model.Video.Id">
|
||||
<i class="bi bi-download"></i>
|
||||
Download
|
||||
</a>
|
||||
<a href="/Account/AddVideoToPlaylist?v=@Model.Video.Id">
|
||||
<i class="bi bi-folder-plus"></i>
|
||||
Save
|
||||
</a>
|
||||
<a href="https://www.youtube.com/watch?v=@Model.Video.Id">
|
||||
<i class="bi bi-share"></i>
|
||||
YouTube link
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="channel-info">
|
||||
<a href="/channel/@Model.Video.Channel.Id" class="avatar">
|
||||
<img src="@Model.Video.Channel.Avatars.LastOrDefault()?.Url">
|
||||
</a>
|
||||
<div class="name">
|
||||
<a href="/channel/@Model.Video.Channel.Id">@Model.Video.Channel.Name</a>
|
||||
</div>
|
||||
<button class="subscribe-button" data-cid="@Model.Video.Channel.Id">Subscribe</button>
|
||||
</div>
|
||||
<p class="description">@Html.Raw(description)</p>
|
||||
</div>
|
||||
<hr>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="video-info">
|
||||
<div class="video-title">@Model.Video.Title</div>
|
||||
<p class="video-sub-info description">
|
||||
<span>@Model.Video.Views @Model.Video.UploadDate</span> @Html.Raw(description)
|
||||
</p>
|
||||
<div class="video-info-buttons">
|
||||
<div>
|
||||
<i class="bi bi-hand-thumbs-up"></i>
|
||||
@Model.Engagement.Likes
|
||||
</div>
|
||||
<div>
|
||||
<i class="bi bi-hand-thumbs-down"></i>
|
||||
@Model.Engagement.Dislikes
|
||||
</div>
|
||||
<a href="/download?v=@Model.Player.Id">
|
||||
<i class="bi bi-download"></i>
|
||||
Download
|
||||
</a>
|
||||
<a href="/Account/AddVideoToPlaylist?v=@Model.Video.Id">
|
||||
<i class="bi bi-folder-plus"></i>
|
||||
Save
|
||||
</a>
|
||||
<a href="https://www.youtube.com/watch?v=@Model.Video.Id">
|
||||
<i class="bi bi-share"></i>
|
||||
YouTube link
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="channel-info__bordered">
|
||||
<a href="/channel/@Model.Video.Channel.Id" class="avatar">
|
||||
<img src="@Model.Video.Channel.Avatars.FirstOrDefault()?.Url">
|
||||
</a>
|
||||
<div class="name">
|
||||
<a href="/channel/@Model.Video.Channel.Id">@Model.Video.Channel.Name</a>
|
||||
</div>
|
||||
<div class="subscriber-count">
|
||||
@Model.Video.Channel.SubscriberCount
|
||||
</div>
|
||||
<button class="subscribe-button" data-cid="@Model.Video.Channel.Id">Subscribe</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="secondary">
|
||||
<noscript>
|
||||
<div class="resolutions-list">
|
||||
<h3>Change Resolution</h3>
|
||||
<div>
|
||||
@foreach (Format format in Model.Player.Formats.Where(x => x.FormatId != "17"))
|
||||
{
|
||||
@if (format.FormatNote == Model.Resolution)
|
||||
{
|
||||
<b>@format.FormatNote (current)</b>
|
||||
}
|
||||
else
|
||||
{
|
||||
<a href="/watch?v=@Model.Player.Id&quality=@format.FormatNote">@format.FormatNote</a>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</noscript>
|
||||
<div class="recommended-list">
|
||||
|
||||
@if (Model.Video.Recommended.Length == 0)
|
||||
{
|
||||
<p style="text-align: center">None :(<br>This is most likely an age-restricted video</p>
|
||||
}
|
||||
@foreach (DynamicItem recommendation in Model.Video.Recommended)
|
||||
{
|
||||
switch (recommendation)
|
||||
{
|
||||
case VideoItem video:
|
||||
<div class="video">
|
||||
<a href="/watch?v=@video.Id" class="thumbnail" style="background-image: url('@video.Thumbnails.LastOrDefault()?.Url')">
|
||||
<span class="video-length">@video.Duration</span>
|
||||
</a>
|
||||
<div class="info">
|
||||
<a href="/watch?v=@video.Id" class="title max-lines-2">@video.Title</a>
|
||||
<div>
|
||||
<a href="/channel/@video.Channel.Id" class="max-lines-1">@video.Channel.Name</a>
|
||||
<div>
|
||||
<span>@video.Views views</span>
|
||||
<span>•</span>
|
||||
<span>@video.UploadedAt</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
break;
|
||||
case PlaylistItem playlist:
|
||||
<div class="playlist">
|
||||
<a href="/watch?v=@playlist.FirstVideoId&list=@playlist.Id" class="thumbnail" style="background-image: url('@playlist.Thumbnails.LastOrDefault()?.Url')">
|
||||
<div>
|
||||
<span>@playlist.VideoCount</span>
|
||||
<span>VIDEOS</span>
|
||||
</div>
|
||||
</a>
|
||||
<div class="info">
|
||||
<a href="/watch?v=@playlist.FirstVideoId&list=@playlist.Id" class="title max-lines-2">@playlist.Title</a>
|
||||
<div>
|
||||
<a href="/channel/@playlist.Channel.Id">@playlist.Channel.Name</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
break;
|
||||
case RadioItem radio:
|
||||
<div class="playlist">
|
||||
<a href="/watch?v=@radio.FirstVideoId&list=@radio.Id" class="thumbnail" style="background-image: url('@radio.Thumbnails.LastOrDefault()?.Url')">
|
||||
<div>
|
||||
<span>MIX</span>
|
||||
</div>
|
||||
</a>
|
||||
<div class="info">
|
||||
<a href="/watch?v=@radio.FirstVideoId&list=@radio.Id" class="title max-lines-2">@radio.Title</a>
|
||||
<div>
|
||||
<span>@radio.Channel.Name</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
break;
|
||||
case ContinuationItem continuationItem:
|
||||
break;
|
||||
default:
|
||||
<div class="video">
|
||||
<div class="thumbnail" style="background-image: url('@recommendation.Thumbnails?.LastOrDefault()?.Url')"></div>
|
||||
<div class="info">
|
||||
<span class="title max-lines-2">@recommendation.GetType().Name</span>
|
||||
<div>
|
||||
<b>WARNING:</b> Unknown recommendation type: @recommendation.Id
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
break;
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (canPlay)
|
||||
{
|
||||
@if (Model.MobileLayout)
|
||||
{
|
||||
<script src="/js/lt-video/player-mobile.js"></script>
|
||||
}
|
||||
else
|
||||
{
|
||||
<script src="/js/lt-video/player-desktop.js"></script>
|
||||
}
|
||||
@if (!Model.CompatibilityMode && !live)
|
||||
{
|
||||
<script src="/js/shaka-player/shaka-player.compiled.min.js"></script>
|
||||
<script>
|
||||
let player = undefined;
|
||||
loadPlayerWithShaka("video", {
|
||||
"id": "@Model.Video.Id",
|
||||
"title": "@Html.Raw(Model.Video.Title.Replace("\"", "\\\""))",
|
||||
"embed": false,
|
||||
"live": false,
|
||||
"storyboard": "/proxy/image?url=@HttpUtility.UrlEncode(Model.Player.Storyboards.FirstOrDefault())"
|
||||
}, [
|
||||
@foreach (Format f in Model.Player.Formats.Reverse())
|
||||
{
|
||||
@:{"height": @f.Resolution.Split("x")[1],"label":"@f.FormatName","src": "/proxy/video?url=@HttpUtility.UrlEncode(f.Url)"},
|
||||
}
|
||||
], "https://@(Context.Request.Host)/manifest/@(Model.Video.Id).mpd").then(x => player = x).catch(alert);
|
||||
</script>
|
||||
}
|
||||
else if (live)
|
||||
{
|
||||
<script src="/js/hls.js/hls.min.js"></script>
|
||||
<script>
|
||||
let player = undefined;
|
||||
loadPlayerWithHls("video", {
|
||||
"id": "@(Model.Video.Id)",
|
||||
"title": "@Html.Raw(Model.Video.Title.Replace("\"", "\\\""))",
|
||||
"embed": false,
|
||||
"live": true
|
||||
}, "https://@(Context.Request.Host)/manifest/@(Model.Video.Id).m3u8").then(x => player = x).catch(alert);
|
||||
</script>
|
||||
}
|
||||
else
|
||||
{
|
||||
<script>
|
||||
const player = new Player("video", {
|
||||
"id": "@Model.Video.Id",
|
||||
"title": "@Html.Raw(Model.Video.Title.Replace("\"", "\\\""))",
|
||||
"embed": false,
|
||||
"live": false,
|
||||
"storyboard": "/proxy/image?url=@HttpUtility.UrlEncode(Model.Player.Storyboards.FirstOrDefault())"
|
||||
}, [
|
||||
@foreach (Format f in Model.Player.Formats.Reverse())
|
||||
{
|
||||
@:{"height": @f.Resolution.Split("x")[1],"label":"@f.FormatName","src": "/proxy/media/@(Model.Player.Id)/@(f.FormatId)"},
|
||||
}
|
||||
]);
|
||||
</script>
|
||||
}
|
||||
}
|
||||
3
core/LightTube/Views/_ViewImports.cshtml
Normal file
3
core/LightTube/Views/_ViewImports.cshtml
Normal file
@@ -0,0 +1,3 @@
|
||||
@using LightTube
|
||||
@using LightTube.Models
|
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
3
core/LightTube/Views/_ViewStart.cshtml
Normal file
3
core/LightTube/Views/_ViewStart.cshtml
Normal file
@@ -0,0 +1,3 @@
|
||||
@{
|
||||
Layout = "_Layout";
|
||||
}
|
||||
Reference in New Issue
Block a user