mirror of
https://github.com/LBRYFoundation/Watch-on-LBRY.git
synced 2025-08-23 17:47:26 +00:00
🍘 YTtoLBRY tool has been remade
This commit is contained in:
parent
c2f894ec61
commit
e2e7426b5b
15 changed files with 299 additions and 266 deletions
|
@ -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 {
|
||||
|
|
|
@ -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
|
|
@ -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>
|
||||
}
|
33
src/common/components/Row/index.tsx
Normal file
33
src/common/components/Row/index.tsx
Normal 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>
|
||||
}
|
6
src/common/components/Row/style.css
Normal file
6
src/common/components/Row/style.css
Normal 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);
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -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%;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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')!)
|
|
@ -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
|
17
src/tools/YTtoLBRY/index.html
Normal file
17
src/tools/YTtoLBRY/index.html
Normal 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>
|
80
src/tools/YTtoLBRY/main.tsx
Normal file
80
src/tools/YTtoLBRY/main.tsx
Normal 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')!)
|
86
src/tools/YTtoLBRY/style.css
Normal file
86
src/tools/YTtoLBRY/style.css
Normal 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;
|
||||
}
|
Loading…
Add table
Reference in a new issue