🍘 YTtoLBRY tool has been remade

This commit is contained in:
Shiba 2022-05-01 14:38:54 +00:00
parent c2f894ec61
commit e2e7426b5b
15 changed files with 299 additions and 266 deletions

View file

@ -10,10 +10,17 @@
}
:root {
font-size: .95rem;
font-family: Arial, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Open Sans, Helvetica Neue, sans-serif;
letter-spacing: .2ch;
}
body#page {
--root-smallest-font-size: .95rem;
--root-font-size-relative-to-screen: 1;
font-size: max(var(--root-smallest-font-size), calc(min(1vw, 2vh) * var(--root-font-size-relative-to-screen)));
}
body {
background: linear-gradient(to left top, var(--color-master), var(--color-slave));
background-attachment: fixed;
@ -24,7 +31,7 @@ body {
body::before {
content: "";
position: absolute;
position: fixed;
inset: 0;
background: rgba(19, 19, 19, 0.75);
}
@ -36,6 +43,30 @@ body::before {
position: relative;
}
a {
cursor: pointer;
}
p,
ul,
ol,
h1,
h2,
h3,
h4,
h5,
h6 {
margin: 0;
}
.options {
display: grid;
width: 100%;
grid-template-columns: repeat(auto-fit, minmax(3em, 1fr));
justify-content: center;
gap: .25em;
padding: 0 1.5em;
}
.button {
display: inline-flex;
@ -48,14 +79,9 @@ body::before {
color: var(--color-light);
border-radius: .5em;
}
border: unset;
.filled {
background: var(--color-gradient-0);
background-clip: text;
-webkit-background-clip: text;
font-weight: bold;
color: transparent;
font-size: inherit;
}
.button.active {
@ -77,13 +103,36 @@ body::before {
pointer-events: none;
}
.options {
.filled {
background: var(--color-gradient-0);
background-clip: text;
-webkit-background-clip: text;
font-weight: bold;
color: transparent;
}
.error {
display: grid;
width: 100%;
grid-template-columns: repeat(auto-fit, minmax(3em, 1fr));
justify-content: center;
gap: .25em;
padding: 0 1.5em;
grid-auto-flow: column;
gap: .5em;
align-items: center;
justify-content: start;
color: var(--color-error);
}
.error::before {
content: "!";
width: 2em;
aspect-ratio: 1/1;
display: grid;
place-items: center;
letter-spacing: 0;
line-height: 0;
border: .1em solid currentColor;
border-radius: 100000vw;
font-weight: bold;
}
.overlay {

View file

@ -1,21 +0,0 @@
@import '../style'
.ButtonRadio
display: flex
justify-content: center
flex-wrap: wrap
gap: .25em
cursor: pointer
*
cursor: pointer
.radio-button
@extend .button
.radio-button.checked
@extend .button.active
input[type="radio"]
opacity: 0
position: absolute

View file

@ -1,31 +0,0 @@
import classnames from 'classnames'
import { h } from 'preact'
import './ButtonRadio.sass'
export interface SelectionOption {
value: string
display: string
}
export interface ButtonRadioProps<T extends string | SelectionOption = string> {
name?: string
onChange(redirect: string): void
value: T extends SelectionOption ? T['value'] : T
options: T[]
}
const getAttr = (x: string | SelectionOption, key: keyof SelectionOption): string => typeof x === 'string' ? x : x[key]
export default function ButtonRadio<T extends string | SelectionOption = string>({ name = 'buttonRadio', onChange, options, value }: ButtonRadioProps<T>) {
/** If it's a string, return the string, if it's a SelectionOption get the selection option property */
return <div className='ButtonRadio'>
{options.map(o => ({ o: getAttr(o, 'value'), display: getAttr(o, 'display') })).map(({ o, display }) =>
<div key={o} className={classnames('radio-button', { 'checked': value === o })}
onClick={() => o !== value && onChange(o)}>
<input name={name} value={o} type='radio' checked={value === o} />
<label>{display}</label>
</div>
)}
</div>
}

View file

@ -0,0 +1,33 @@
import { ComponentChildren, h } from 'preact'
import './style.css'
/*
Re-implementation of https://github.com/DeepDoge/svelte-responsive-row
*/
export function Row(params: {
children: ComponentChildren
type?: "fit" | "fill",
idealSize?: string,
gap?: string,
maxColumnCount?: number,
justifyItems?: "center" | "start" | "end" | "stretch"
}) {
if (!params.type) params.type = 'fill'
if (!params.gap) params.gap = '0'
if (!params.idealSize) params.idealSize = '100%'
if (!params.maxColumnCount) params.maxColumnCount = Number.MAX_SAFE_INTEGER
if (!params.justifyItems) params.justifyItems = 'center'
return <div className='responsive-row'
style={{
'--ideal-size': params.idealSize,
'--gap': params.gap === "0" ? "0px" : params.gap,
'--type': `auto-${params.type}`,
'--max-column-count': params.maxColumnCount,
'--justify-items': params.justifyItems
}}
>
{params.children}
</div>
}

View file

@ -0,0 +1,6 @@
.responsive-row {
display: grid;
grid-template-columns: repeat(var(--type), minmax(min(max(100% / var(--max-column-count) - var(--gap), var(--ideal-size)), 100%), 1fr));
gap: var(--gap);
justify-items: var(--justify-items);
}

View file

@ -42,7 +42,7 @@
},
"web_accessible_resources": [
"popup.html",
"tools/YTtoLBRY.html",
"tools/YTtoLBRY/index.html",
"icons/lbry/lbry-logo.svg",
"icons/lbry/odysee-logo.svg",
"icons/lbry/madiator-logo.svg"

View file

@ -1,18 +1,3 @@
label {
font-size: 1.75em;
font-weight: bold;
text-align: center;
}
a {
cursor: pointer;
}
p {
margin: 0;
text-align: center;
}
header {
display: grid;
gap: .5em;
@ -34,6 +19,12 @@ section {
gap: .75em;
}
section label {
font-size: 1.75em;
font-weight: bold;
text-align: center;
}
#popup {
width: 35em;
max-width: 100%;

View file

@ -40,20 +40,21 @@ function WatchOnLbryPopup(params: { profile: Awaited<ReturnType<typeof getProfil
<label>{nickname}</label>
<p>{friendlyPublicKey(publicKey)}</p>
<span><b>Score: {params.profile?.score ?? '...'}</b> - <a target='_blank' href="https://finder.madiator.com/leaderboard" class="filled">🔗Leaderboard</a></span>
{ urlResolver !== 'madiatorFinder' && <span class="error">You need to use Madiator Finder API for scoring to work</span> }
</section>
<section>
{
popupRoute === 'profile'
? <a onClick={() => updateRoute('')} className="button filled"> Back</a>
: <a className='button filled' onClick={() => updateRoute('profile')} href="#profile">Profile Settings</a>
? <a onClick={() => updateRoute('')} className="filled"> Back</a>
: <a className='filled' onClick={() => updateRoute('profile')} href="#profile">Profile Settings</a>
}
</section>
</header>
: <header>
{
popupRoute === 'profile'
? <a onClick={() => updateRoute('')} className="button filled"> Back</a>
: <a className='button filled' onClick={() => updateRoute('profile')} href="#profile">Your Profile</a>
? <a onClick={() => updateRoute('')} className="filled"> Back</a>
: <a className='filled' onClick={() => updateRoute('profile')} href="#profile">Profile Settings</a>
}
</header>
}
@ -156,7 +157,7 @@ function WatchOnLbryPopup(params: { profile: Awaited<ReturnType<typeof getProfil
</section>
<section>
<label>Tools</label>
<a target='_blank' href='/tools/YTtoLBRY.html' className={`button filled`}>
<a target='_blank' href='/tools/YTtoLBRY/index.html' className={`filled`}>
Subscription Converter
</a>
</section>

View file

@ -1,93 +0,0 @@
body {
--color-text: whitesmoke;
--color-backround: rgb(28, 31, 34);
--color-card: #2a2e32;
--color-primary: rgb(43, 187, 144);
background-color: var(--color-backround);
color: var(--color-text);
font-size: 1rem;
}
a {
color: var(--color-primary);
}
h1, h2, h3, h4, h5, h6 {
margin: 0 0 10px;
}
.container {
display: block;
text-align: center;
margin: auto;
width: 50%;
margin-bottom: 15px;
border: 3px;
padding: 10px;
}
.YTtoLBRY {
display: flex;
justify-content: space-around;
flex-direction: row;
}
.Conversion {
margin: 1rem;
width: 45em;
}
.ConversionHelp {
display: flex;
flex-direction: column;
align-items: center;
}
.ConversionCard {
box-shadow: 0 0 0 1px rgba(16,22,26,.1), 0 1px 1px rgba(16,22,26,.2), 0 2px 6px rgba(16,22,26,.2);
border-radius: 5px;
background-color: var(--color-card);
padding: 20px;
}
.btn {
color: var(--color-text);
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
border: 0;
padding: 5px 10px;
border-radius: 3px;
font-size: 14px;
min-width: 30px;
min-height: 30px;
}
.progress-text {
font-weight: bold;
padding: .25em 0;
}
.btn.btn-primary {
background-color: var(--color-primary);
}
.btn:disabled {
background-color: rgba(200, 200, 200, .5);
color: rgba(90, 90, 90, .6);
cursor: not-allowed;
}
@media (max-width: 1400px) {
.YTtoLBRY {
flex-direction: column;
}
.Conversion {
width: auto;
}
}

View file

@ -1,15 +0,0 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title>Subscription Converter</title>
<link rel="stylesheet" href="YTtoLBRY.css" />
<script src="YTtoLBRY.tsx" charset="utf-8" defer></script>
</head>
<body>
<div id="root" />
</body>
</html>

View file

@ -1,70 +0,0 @@
import { h, render } from 'preact'
import { useState } from 'preact/hooks'
import { getExtensionSettingsAsync, targetPlatformSettings } from '../common/settings'
import { getFileContent, getSubsFromCsv, getSubsFromJson, getSubsFromOpml } from '../common/yt'
import { resolveById } from '../common/yt/urlResolve'
import readme from './README.md'
/**
* Parses the subscription file and queries the API for lbry channels
*
* @param file to read
* @returns a promise with the list of channels that were found on lbry
*/
async function lbryChannelsFromFile(file: File) {
const ext = file.name.split('.').pop()?.toLowerCase()
const ids = new Set((
ext === 'xml' || ext == 'opml' ? getSubsFromOpml :
ext === 'csv' ? getSubsFromCsv :
getSubsFromJson)(await getFileContent(file)))
const items = await resolveById(
Array.from(ids).map(id => ({ id, type: 'channel' } as const)),
(progress) => render(<YTtoLBRY progress={progress} />, document.getElementById('root')!))
const { targetPlatform: platform } = await getExtensionSettingsAsync()
const urlPrefix = targetPlatformSettings[platform].domainPrefix
return Object.values(items).map((item) => urlPrefix + item.id)
}
function ConversionCard({ onSelect, progress }: { onSelect(file: File): Promise<void> | void, progress: number }) {
const [file, setFile] = useState(null as File | null)
const [isLoading, setLoading] = useState(false)
return <div className='ConversionCard'>
<h2>Select YouTube Subscriptions</h2>
<div style={{ marginBottom: 10 }}>
<input type='file' onChange={e => setFile(e.currentTarget.files?.length ? e.currentTarget.files[0] : null)} />
</div>
<button class='btn btn-primary' children='Start Conversion!' disabled={!file || isLoading} onClick={async () => {
if (!file) return
setLoading(true)
await onSelect(file)
setLoading(false)
}} />
<div class="progress-text">
{progress > 0 ? `${(progress * 100).toFixed(1)}%` : ''}
</div>
</div>
}
function YTtoLBRY({ progress }: { progress: number }) {
const [lbryChannels, setLbryChannels] = useState([] as string[])
return <div className='YTtoLBRY'>
<div className='Conversion'>
<ConversionCard progress={progress} onSelect={async file => setLbryChannels(await lbryChannelsFromFile(file))} />
<ul>
{lbryChannels.map((x, i) => <li key={i} children={<a href={x} children={x} />} />)}
</ul>
</div>
<div className='ConversionHelp'>
<iframe width='712px' height='400px' allowFullScreen
src='https://lbry.tv/$/embed/howtouseconverter/c9827448d6ac7a74ecdb972c5cdf9ddaf648a28e' />
<section dangerouslySetInnerHTML={{ __html: readme }} />
</div>
</div>
}
render(<YTtoLBRY progress={0} />, document.getElementById('root')!)

View file

@ -3,4 +3,4 @@
1. Go to https://takeout.google.com/settings/takeout
2. Deselect everything except `YouTube and YouTube Music` and within that only select `subscriptions`
3. Go through the process and create the export
4. Once it's exported, open the archive and find `YouTube and YouTube Music/subscriptions/subscriptions.json` and upload it to the extension
4. Once it's exported, open the archive and find `YouTube and YouTube Music/subscriptions/subscriptions.json` and upload it to the extension

View file

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title>Subscription Converter</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="style.css" />
<link rel="stylesheet" href="../../common/common.css" />
<script src="main.tsx" charset="utf-8" defer></script>
</head>
<body id="page">
<div id="root" />
</body>
</html>

View file

@ -0,0 +1,80 @@
import { h, render } from 'preact'
import { useState } from 'preact/hooks'
import { targetPlatformSettings } from '../../common/settings'
import { useLbrySettings } from '../../common/useSettings'
import { getFileContent, getSubsFromCsv, getSubsFromJson, getSubsFromOpml } from '../../common/yt'
import { resolveById } from '../../common/yt/urlResolve'
import readme from './README.md'
async function getSubscribedChannelIdsFromFile(file: File) {
const ext = file.name.split('.').pop()?.toLowerCase()
const content = await getFileContent(file)
switch (ext) {
case 'xml':
case 'opml':
return getSubsFromOpml(content)
case 'csv':
return getSubsFromCsv(content)
default:
return getSubsFromJson(content)
}
}
async function findChannels(channelIds: string[], progressCallback: Parameters<typeof resolveById>['1']) {
const resultItems = await resolveById(channelIds.map((channelId) => ({ id: channelId, type: 'channel' })), progressCallback)
return Object.values(resultItems).map((item) => item.id)
}
function Conversion() {
const [file, setFile] = useState(null as File | null)
const [progress, setProgress] = useState(0)
const [lbryChannelIds, setLbryChannels] = useState([] as Awaited<ReturnType<typeof findChannels>>)
const settings = useLbrySettings()
let loading = progress > 0 && progress !== 1
return <div className='conversion'>
<form onSubmit={async (event) => {
event.preventDefault()
if (file) setLbryChannels(await findChannels(await getSubscribedChannelIdsFromFile(file), (progress) => setProgress(progress)))
}}
>
<div class="fields">
<label for="conversion-file">Select YouTube Subscriptions</label>
<input id="conversion-file" type='file' onChange={event => setFile(event.currentTarget.files?.length ? event.currentTarget.files[0] : null)} />
</div>
<div class="actions">
<button className={`button ${!file || progress > 0 ? '' : 'active'}`} disabled={!file || loading}>
{loading ? `${(progress * 100).toFixed(1)}%` : 'Start Conversion!'}
</button>
</div>
</form>
{
progress === 1 &&
<div class="results">
<b>Results:</b>
{
lbryChannelIds.length > 0
? lbryChannelIds.map((lbryChannelId) =>
<article class="result-item">
<a href={`${targetPlatformSettings[settings.targetPlatform].domainPrefix}${lbryChannelId}`}>{lbryChannelId}</a>
</article>)
: <span class="error">No Result</span>
}
</div>
}
</div>
}
function YTtoLBRY() {
return <main>
<Conversion />
<aside class="help">
<iframe allowFullScreen
src='https://lbry.tv/$/embed/howtouseconverter/c9827448d6ac7a74ecdb972c5cdf9ddaf648a28e' />
<section dangerouslySetInnerHTML={{ __html: readme }} />
</aside>
</main>
}
render(<YTtoLBRY />, document.getElementById('root')!)

View file

@ -0,0 +1,86 @@
main {
display: flex;
flex-wrap: wrap-reverse;
min-height: 100vh;
gap: 2em;
padding: 1em;
overflow-wrap: break-word;
align-items: start;
}
.conversion,
.help {
flex: 1;
min-width: 40em;
display: grid;
gap: 1em;
}
.conversion>*,
.help>* {
background-color: rgba(0, 0, 0, 0.5);
border-radius: .5em;
padding: .5em;
}
.help iframe {
width: 100%;
max-width: 100%;
aspect-ratio: 16/9;
max-height: 50vh;
border: none;
}
.conversion {
height: 100%;
}
.conversion form {
display: grid;
gap: 1em;
justify-items: center;
}
.conversion form .fields {
display: grid;
gap: .5em;
}
.conversion form .actions {
display: flex;
flex-wrap: wrap;
gap: .5em;
}
.conversion form .actions::after {
content: "";
flex-grow: 100000000000000000;
}
.conversion form button {
cursor: pointer;
}
.conversion form button:disabled {
cursor: not-allowed;
}
.conversion .results {
display: grid;
gap: .5em;
}
.help a,
.conversion .results a {
background: var(--color-gradient-0);
background-clip: text;
-webkit-background-clip: text;
color: transparent;
font-weight: bold;
}
.conversion .results .result-item {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}