parent
55c77395bb
commit
f5af0deb31
@ -0,0 +1,534 @@ |
|||||||
|
/* |
||||||
|
* SPDX-License-Identifier: GPL-3.0 |
||||||
|
* Aerocord, a vesktop fork for older microsoft NT releases such as NT 6.0, 6.1, 6.2 and 6.3.
|
||||||
|
* Credits to vendicated and the rest of the vesktop contribuitors for making Vesktop! |
||||||
|
*/ |
||||||
|
|
||||||
|
import "./screenSharePicker.css"; |
||||||
|
|
||||||
|
import { closeModal, Logger, Margins, Modals, ModalSize, openModal, useAwaiter } from "@vencord/types/utils"; |
||||||
|
import { findStoreLazy, onceReady } from "@vencord/types/webpack"; |
||||||
|
import { |
||||||
|
Button, |
||||||
|
Card, |
||||||
|
FluxDispatcher, |
||||||
|
Forms, |
||||||
|
Select, |
||||||
|
Switch, |
||||||
|
Text, |
||||||
|
UserStore, |
||||||
|
useState |
||||||
|
} from "@vencord/types/webpack/common"; |
||||||
|
import type { Dispatch, SetStateAction } from "react"; |
||||||
|
import { addPatch } from "renderer/patches/shared"; |
||||||
|
import { isLinux, isWindows } from "renderer/utils"; |
||||||
|
|
||||||
|
const StreamResolutions = ["480", "720", "1080", "1440"] as const; |
||||||
|
const StreamFps = ["15", "30", "60"] as const; |
||||||
|
|
||||||
|
const MediaEngineStore = findStoreLazy("MediaEngineStore"); |
||||||
|
|
||||||
|
export type StreamResolution = (typeof StreamResolutions)[number]; |
||||||
|
export type StreamFps = (typeof StreamFps)[number]; |
||||||
|
|
||||||
|
interface StreamSettings { |
||||||
|
resolution: StreamResolution; |
||||||
|
fps: StreamFps; |
||||||
|
audio: boolean; |
||||||
|
audioSource?: string; |
||||||
|
contentHint?: string; |
||||||
|
workaround?: boolean; |
||||||
|
onlyDefaultSpeakers?: boolean; |
||||||
|
} |
||||||
|
|
||||||
|
export interface StreamPick extends StreamSettings { |
||||||
|
id: string; |
||||||
|
} |
||||||
|
|
||||||
|
interface Source { |
||||||
|
id: string; |
||||||
|
name: string; |
||||||
|
url: string; |
||||||
|
} |
||||||
|
|
||||||
|
export let currentSettings: StreamSettings | null = null; |
||||||
|
|
||||||
|
const logger = new Logger("VesktopScreenShare"); |
||||||
|
|
||||||
|
addPatch({ |
||||||
|
patches: [ |
||||||
|
{ |
||||||
|
find: "this.localWant=", |
||||||
|
replacement: { |
||||||
|
match: /this.localWant=/, |
||||||
|
replace: "$self.patchStreamQuality(this);$&" |
||||||
|
} |
||||||
|
}, |
||||||
|
{ |
||||||
|
find: "x-google-max-bitrate", |
||||||
|
replacement: [ |
||||||
|
{ |
||||||
|
// eslint-disable-next-line no-useless-escape
|
||||||
|
match: /"x-google-max-bitrate=".concat\(\i\)/, |
||||||
|
replace: '"x-google-max-bitrate=".concat("80_000")' |
||||||
|
}, |
||||||
|
{ |
||||||
|
match: /;level-asymmetry-allowed=1/, |
||||||
|
replace: ";b=AS:800000;level-asymmetry-allowed=1" |
||||||
|
} |
||||||
|
] |
||||||
|
} |
||||||
|
], |
||||||
|
patchStreamQuality(opts: any) { |
||||||
|
if (!currentSettings) return; |
||||||
|
|
||||||
|
const framerate = Number(currentSettings.fps); |
||||||
|
const height = Number(currentSettings.resolution); |
||||||
|
const width = Math.round(height * (16 / 9)); |
||||||
|
|
||||||
|
Object.assign(opts, { |
||||||
|
bitrateMin: 500000, |
||||||
|
bitrateMax: 8000000, |
||||||
|
bitrateTarget: 600000 |
||||||
|
}); |
||||||
|
if (opts?.encode) { |
||||||
|
Object.assign(opts.encode, { |
||||||
|
framerate, |
||||||
|
width, |
||||||
|
height, |
||||||
|
pixelCount: height * width |
||||||
|
}); |
||||||
|
} |
||||||
|
Object.assign(opts.capture, { |
||||||
|
framerate, |
||||||
|
width, |
||||||
|
height, |
||||||
|
pixelCount: height * width |
||||||
|
}); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
if (isLinux) { |
||||||
|
onceReady.then(() => { |
||||||
|
FluxDispatcher.subscribe("STREAM_CLOSE", ({ streamKey }: { streamKey: string }) => { |
||||||
|
const owner = streamKey.split(":").at(-1); |
||||||
|
|
||||||
|
if (owner !== UserStore.getCurrentUser().id) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
VesktopNative.virtmic.stop(); |
||||||
|
}); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
export function openScreenSharePicker(screens: Source[], skipPicker: boolean) { |
||||||
|
let didSubmit = false; |
||||||
|
return new Promise<StreamPick>((resolve, reject) => { |
||||||
|
const key = openModal( |
||||||
|
props => ( |
||||||
|
<ModalComponent |
||||||
|
screens={screens} |
||||||
|
modalProps={props} |
||||||
|
submit={async v => { |
||||||
|
didSubmit = true; |
||||||
|
if (v.audioSource && v.audioSource !== "None") { |
||||||
|
if (v.audioSource === "Entire System") { |
||||||
|
await VesktopNative.virtmic.startSystem(v.workaround); |
||||||
|
} else { |
||||||
|
await VesktopNative.virtmic.start([v.audioSource], v.workaround); |
||||||
|
} |
||||||
|
} |
||||||
|
resolve(v); |
||||||
|
}} |
||||||
|
close={() => { |
||||||
|
props.onClose(); |
||||||
|
if (!didSubmit) reject("Aborted"); |
||||||
|
}} |
||||||
|
skipPicker={skipPicker} |
||||||
|
/> |
||||||
|
), |
||||||
|
{ |
||||||
|
onCloseRequest() { |
||||||
|
closeModal(key); |
||||||
|
reject("Aborted"); |
||||||
|
} |
||||||
|
} |
||||||
|
); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
function ScreenPicker({ screens, chooseScreen }: { screens: Source[]; chooseScreen: (id: string) => void }) { |
||||||
|
return ( |
||||||
|
<div className="vcd-screen-picker-grid"> |
||||||
|
{screens.map(({ id, name, url }) => ( |
||||||
|
<label key={id}> |
||||||
|
<input type="radio" name="screen" value={id} onChange={() => chooseScreen(id)} /> |
||||||
|
|
||||||
|
<img src={url} alt="" /> |
||||||
|
<Text variant="text-sm/normal">{name}</Text> |
||||||
|
</label> |
||||||
|
))} |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
function StreamSettings({ |
||||||
|
source, |
||||||
|
settings, |
||||||
|
setSettings, |
||||||
|
skipPicker |
||||||
|
}: { |
||||||
|
source: Source; |
||||||
|
settings: StreamSettings; |
||||||
|
setSettings: Dispatch<SetStateAction<StreamSettings>>; |
||||||
|
skipPicker: boolean; |
||||||
|
}) { |
||||||
|
const [thumb] = useAwaiter( |
||||||
|
() => (skipPicker ? Promise.resolve(source.url) : VesktopNative.capturer.getLargeThumbnail(source.id)), |
||||||
|
{ |
||||||
|
fallbackValue: source.url, |
||||||
|
deps: [source.id] |
||||||
|
} |
||||||
|
); |
||||||
|
|
||||||
|
return ( |
||||||
|
<div className="vcd-screen-picker-settings-grid"> |
||||||
|
<div> |
||||||
|
<Forms.FormTitle>What you're streaming</Forms.FormTitle> |
||||||
|
<Card className="vcd-screen-picker-card vcd-screen-picker-preview"> |
||||||
|
<img src={thumb} alt="" /> |
||||||
|
<Text variant="text-sm/normal">{source.name}</Text> |
||||||
|
</Card> |
||||||
|
|
||||||
|
<Forms.FormTitle>Stream Settings</Forms.FormTitle> |
||||||
|
|
||||||
|
<Card className="vcd-screen-picker-card"> |
||||||
|
<div className="vcd-screen-picker-quality"> |
||||||
|
<section> |
||||||
|
<Forms.FormTitle>Resolution</Forms.FormTitle> |
||||||
|
<div className="vcd-screen-picker-radios"> |
||||||
|
{StreamResolutions.map(res => ( |
||||||
|
<label |
||||||
|
className="vcd-screen-picker-radio" |
||||||
|
data-checked={settings.resolution === res} |
||||||
|
> |
||||||
|
<Text variant="text-sm/bold">{res}</Text> |
||||||
|
<input |
||||||
|
type="radio" |
||||||
|
name="resolution" |
||||||
|
value={res} |
||||||
|
checked={settings.resolution === res} |
||||||
|
onChange={() => setSettings(s => ({ ...s, resolution: res }))} |
||||||
|
/> |
||||||
|
</label> |
||||||
|
))} |
||||||
|
</div> |
||||||
|
</section> |
||||||
|
|
||||||
|
<section> |
||||||
|
<Forms.FormTitle>Frame Rate</Forms.FormTitle> |
||||||
|
<div className="vcd-screen-picker-radios"> |
||||||
|
{StreamFps.map(fps => ( |
||||||
|
<label className="vcd-screen-picker-radio" data-checked={settings.fps === fps}> |
||||||
|
<Text variant="text-sm/bold">{fps}</Text> |
||||||
|
<input |
||||||
|
type="radio" |
||||||
|
name="fps" |
||||||
|
value={fps} |
||||||
|
checked={settings.fps === fps} |
||||||
|
onChange={() => setSettings(s => ({ ...s, fps }))} |
||||||
|
/> |
||||||
|
</label> |
||||||
|
))} |
||||||
|
</div> |
||||||
|
</section> |
||||||
|
</div> |
||||||
|
<div className="vcd-screen-picker-quality"> |
||||||
|
<section> |
||||||
|
<Forms.FormTitle>Content Type</Forms.FormTitle> |
||||||
|
<div> |
||||||
|
<div className="vcd-screen-picker-radios"> |
||||||
|
<label |
||||||
|
className="vcd-screen-picker-radio" |
||||||
|
data-checked={settings.contentHint === "motion"} |
||||||
|
> |
||||||
|
<Text variant="text-sm/bold">Prefer Smoothness</Text> |
||||||
|
<input |
||||||
|
type="radio" |
||||||
|
name="contenthint" |
||||||
|
value="motion" |
||||||
|
checked={settings.contentHint === "motion"} |
||||||
|
onChange={() => setSettings(s => ({ ...s, contentHint: "motion" }))} |
||||||
|
/> |
||||||
|
</label> |
||||||
|
<label |
||||||
|
className="vcd-screen-picker-radio" |
||||||
|
data-checked={settings.contentHint === "detail"} |
||||||
|
> |
||||||
|
<Text variant="text-sm/bold">Prefer Clarity</Text> |
||||||
|
<input |
||||||
|
type="radio" |
||||||
|
name="contenthint" |
||||||
|
value="detail" |
||||||
|
checked={settings.contentHint === "detail"} |
||||||
|
onChange={() => setSettings(s => ({ ...s, contentHint: "detail" }))} |
||||||
|
/> |
||||||
|
</label> |
||||||
|
</div> |
||||||
|
<div className="vcd-screen-picker-hint-description"> |
||||||
|
<p> |
||||||
|
Choosing "Prefer Clarity" will result in a significantly lower framerate in |
||||||
|
exchange for a much sharper and clearer image. |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</section> |
||||||
|
</div> |
||||||
|
</Card> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div> |
||||||
|
{isWindows && ( |
||||||
|
<Switch |
||||||
|
value={settings.audio} |
||||||
|
onChange={checked => setSettings(s => ({ ...s, audio: checked }))} |
||||||
|
hideBorder |
||||||
|
className="vcd-screen-picker-audio" |
||||||
|
> |
||||||
|
Stream With Audio |
||||||
|
</Switch> |
||||||
|
)} |
||||||
|
|
||||||
|
{isLinux && ( |
||||||
|
<AudioSourcePickerLinux |
||||||
|
audioSource={settings.audioSource} |
||||||
|
workaround={settings.workaround} |
||||||
|
onlyDefaultSpeakers={settings.onlyDefaultSpeakers} |
||||||
|
setAudioSource={source => setSettings(s => ({ ...s, audioSource: source }))} |
||||||
|
setWorkaround={value => setSettings(s => ({ ...s, workaround: value }))} |
||||||
|
setOnlyDefaultSpeakers={value => setSettings(s => ({ ...s, onlyDefaultSpeakers: value }))} |
||||||
|
/> |
||||||
|
)} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
function AudioSourcePickerLinux({ |
||||||
|
audioSource, |
||||||
|
workaround, |
||||||
|
onlyDefaultSpeakers, |
||||||
|
setAudioSource, |
||||||
|
setWorkaround, |
||||||
|
setOnlyDefaultSpeakers |
||||||
|
}: { |
||||||
|
audioSource?: string; |
||||||
|
workaround?: boolean; |
||||||
|
onlyDefaultSpeakers?: boolean; |
||||||
|
setAudioSource(s: string): void; |
||||||
|
setWorkaround(b: boolean): void; |
||||||
|
setOnlyDefaultSpeakers(b: boolean): void; |
||||||
|
}) { |
||||||
|
const [sources, _, loading] = useAwaiter(() => VesktopNative.virtmic.list(), { |
||||||
|
fallbackValue: { ok: true, targets: [], hasPipewirePulse: true } |
||||||
|
}); |
||||||
|
|
||||||
|
const allSources = sources.ok ? ["None", "Entire System", ...sources.targets] : null; |
||||||
|
const hasPipewirePulse = sources.ok ? sources.hasPipewirePulse : true; |
||||||
|
|
||||||
|
const [ignorePulseWarning, setIgnorePulseWarning] = useState(false); |
||||||
|
|
||||||
|
return ( |
||||||
|
<> |
||||||
|
<Forms.FormTitle>Audio Settings</Forms.FormTitle> |
||||||
|
<Card className="vcd-screen-picker-card"> |
||||||
|
{loading ? ( |
||||||
|
<Forms.FormTitle>Loading Audio Sources...</Forms.FormTitle> |
||||||
|
) : ( |
||||||
|
<Forms.FormTitle>Audio Source</Forms.FormTitle> |
||||||
|
)} |
||||||
|
|
||||||
|
{!sources.ok && sources.isGlibCxxOutdated && ( |
||||||
|
<Forms.FormText> |
||||||
|
Failed to retrieve Audio Sources because your C++ library is too old to run |
||||||
|
<a href="https://github.com/Vencord/venmic" target="_blank"> |
||||||
|
venmic |
||||||
|
</a> |
||||||
|
. See{" "} |
||||||
|
<a href="https://gist.github.com/Vendicated/b655044ffbb16b2716095a448c6d827a" target="_blank"> |
||||||
|
this guide |
||||||
|
</a>{" "} |
||||||
|
for possible solutions. |
||||||
|
</Forms.FormText> |
||||||
|
)} |
||||||
|
|
||||||
|
{hasPipewirePulse || ignorePulseWarning ? ( |
||||||
|
allSources && ( |
||||||
|
<Select |
||||||
|
options={allSources.map(s => ({ label: s, value: s, default: s === "None" }))} |
||||||
|
isSelected={s => s === audioSource} |
||||||
|
select={setAudioSource} |
||||||
|
serialize={String} |
||||||
|
/> |
||||||
|
) |
||||||
|
) : ( |
||||||
|
<Text variant="text-sm/normal"> |
||||||
|
Could not find pipewire-pulse. This usually means that you do not run pipewire as your main |
||||||
|
audio-server. <br /> |
||||||
|
You can still continue, however, please beware that you can only share audio of apps that are |
||||||
|
running under pipewire. |
||||||
|
<br /> |
||||||
|
<a onClick={() => setIgnorePulseWarning(true)}>I know what I'm doing</a> |
||||||
|
</Text> |
||||||
|
)} |
||||||
|
|
||||||
|
<Forms.FormDivider className={Margins.top16 + " " + Margins.bottom16} /> |
||||||
|
|
||||||
|
<Switch |
||||||
|
onChange={setWorkaround} |
||||||
|
value={workaround ?? false} |
||||||
|
note={ |
||||||
|
<> |
||||||
|
Work around an issue that causes the microphone to be shared instead of the correct audio. |
||||||
|
Only enable if you're experiencing this issue. |
||||||
|
</> |
||||||
|
} |
||||||
|
> |
||||||
|
Microphone Workaround |
||||||
|
</Switch> |
||||||
|
|
||||||
|
<Switch |
||||||
|
hideBorder |
||||||
|
onChange={setOnlyDefaultSpeakers} |
||||||
|
disabled={audioSource !== "Entire System"} |
||||||
|
value={onlyDefaultSpeakers ?? true} |
||||||
|
note={ |
||||||
|
<> |
||||||
|
When sharing entire desktop audio, only share apps that play to the default speakers and |
||||||
|
ignore apps that play to other speakers or devices. |
||||||
|
</> |
||||||
|
} |
||||||
|
> |
||||||
|
Only Default Speakers |
||||||
|
</Switch> |
||||||
|
</Card> |
||||||
|
</> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
function ModalComponent({ |
||||||
|
screens, |
||||||
|
modalProps, |
||||||
|
submit, |
||||||
|
close, |
||||||
|
skipPicker |
||||||
|
}: { |
||||||
|
screens: Source[]; |
||||||
|
modalProps: any; |
||||||
|
submit: (data: StreamPick) => void; |
||||||
|
close: () => void; |
||||||
|
skipPicker: boolean; |
||||||
|
}) { |
||||||
|
const [selected, setSelected] = useState<string | undefined>(skipPicker ? screens[0].id : void 0); |
||||||
|
const [settings, setSettings] = useState<StreamSettings>({ |
||||||
|
resolution: "720", |
||||||
|
fps: "30", |
||||||
|
contentHint: "motion", |
||||||
|
audio: true |
||||||
|
}); |
||||||
|
|
||||||
|
return ( |
||||||
|
<Modals.ModalRoot {...modalProps} size={ModalSize.MEDIUM}> |
||||||
|
<Modals.ModalHeader className="vcd-screen-picker-header"> |
||||||
|
<Forms.FormTitle tag="h2">ScreenShare</Forms.FormTitle> |
||||||
|
<Modals.ModalCloseButton onClick={close} /> |
||||||
|
</Modals.ModalHeader> |
||||||
|
<Modals.ModalContent className="vcd-screen-picker-modal"> |
||||||
|
{!selected ? ( |
||||||
|
<ScreenPicker screens={screens} chooseScreen={setSelected} /> |
||||||
|
) : ( |
||||||
|
<StreamSettings |
||||||
|
source={screens.find(s => s.id === selected)!} |
||||||
|
settings={settings} |
||||||
|
setSettings={setSettings} |
||||||
|
skipPicker={skipPicker} |
||||||
|
/> |
||||||
|
)} |
||||||
|
</Modals.ModalContent> |
||||||
|
<Modals.ModalFooter className="vcd-screen-picker-footer"> |
||||||
|
<Button |
||||||
|
disabled={!selected} |
||||||
|
onClick={() => { |
||||||
|
currentSettings = settings; |
||||||
|
try { |
||||||
|
const frameRate = Number(settings.fps); |
||||||
|
const height = Number(settings.resolution); |
||||||
|
const width = Math.round(height * (16 / 9)); |
||||||
|
|
||||||
|
const conn = [...MediaEngineStore.getMediaEngine().connections].find( |
||||||
|
connection => connection.streamUserId === UserStore.getCurrentUser().id |
||||||
|
); |
||||||
|
|
||||||
|
if (conn) { |
||||||
|
conn.videoStreamParameters[0].maxFrameRate = frameRate; |
||||||
|
conn.videoStreamParameters[0].maxResolution.height = height; |
||||||
|
conn.videoStreamParameters[0].maxResolution.width = width; |
||||||
|
} |
||||||
|
|
||||||
|
submit({ |
||||||
|
id: selected!, |
||||||
|
...settings |
||||||
|
}); |
||||||
|
|
||||||
|
setTimeout(async () => { |
||||||
|
const conn = [...MediaEngineStore.getMediaEngine().connections].find( |
||||||
|
connection => connection.streamUserId === UserStore.getCurrentUser().id |
||||||
|
); |
||||||
|
if (!conn) return; |
||||||
|
|
||||||
|
const track = conn.input.stream.getVideoTracks()[0]; |
||||||
|
|
||||||
|
const constraints = { |
||||||
|
...track.getConstraints(), |
||||||
|
frameRate, |
||||||
|
width: { min: 640, ideal: width, max: width }, |
||||||
|
height: { min: 480, ideal: height, max: height }, |
||||||
|
advanced: [{ width: width, height: height }], |
||||||
|
resizeMode: "none" |
||||||
|
}; |
||||||
|
|
||||||
|
try { |
||||||
|
await track.applyConstraints(constraints); |
||||||
|
|
||||||
|
logger.info( |
||||||
|
"Applied constraints successfully. New constraints:", |
||||||
|
track.getConstraints() |
||||||
|
); |
||||||
|
} catch (e) { |
||||||
|
logger.error("Failed to apply constraints.", e); |
||||||
|
} |
||||||
|
}, 100); |
||||||
|
} catch (error) { |
||||||
|
logger.error("Error while submitting stream.", error); |
||||||
|
} |
||||||
|
|
||||||
|
close(); |
||||||
|
}} |
||||||
|
> |
||||||
|
Go Live |
||||||
|
</Button> |
||||||
|
|
||||||
|
{selected && !skipPicker ? ( |
||||||
|
<Button color={Button.Colors.TRANSPARENT} onClick={() => setSelected(void 0)}> |
||||||
|
Back |
||||||
|
</Button> |
||||||
|
) : ( |
||||||
|
<Button color={Button.Colors.TRANSPARENT} onClick={close}> |
||||||
|
Cancel |
||||||
|
</Button> |
||||||
|
)} |
||||||
|
</Modals.ModalFooter> |
||||||
|
</Modals.ModalRoot> |
||||||
|
); |
||||||
|
} |
@ -0,0 +1,7 @@ |
|||||||
|
/* |
||||||
|
* SPDX-License-Identifier: GPL-3.0 |
||||||
|
* Aerocord, a vesktop fork for older microsoft NT releases such as NT 6.0, 6.1, 6.2 and 6.3.
|
||||||
|
* Credits to vendicated and the rest of the vesktop contribuitors for making Vesktop! |
||||||
|
*/ |
||||||
|
|
||||||
|
export * as ScreenShare from "./ScreenSharePicker"; |
@ -0,0 +1,146 @@ |
|||||||
|
.vcd-screen-picker-modal { |
||||||
|
padding: 1em; |
||||||
|
} |
||||||
|
|
||||||
|
.vcd-screen-picker-header h1 { |
||||||
|
margin: 0; |
||||||
|
} |
||||||
|
|
||||||
|
.vcd-screen-picker-footer { |
||||||
|
display: flex; |
||||||
|
gap: 1em; |
||||||
|
} |
||||||
|
|
||||||
|
.vcd-screen-picker-settings-grid { |
||||||
|
gap: 1em; |
||||||
|
display: grid; |
||||||
|
grid-template-columns: 1fr 1fr; |
||||||
|
} |
||||||
|
|
||||||
|
.vcd-screen-picker-settings-grid > div { |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
} |
||||||
|
|
||||||
|
.vcd-screen-picker-card { |
||||||
|
flex-grow: 1; |
||||||
|
} |
||||||
|
|
||||||
|
.vcd-screen-picker-grid { |
||||||
|
display: grid; |
||||||
|
grid-template-columns: 1fr 1fr; |
||||||
|
gap: 2em 1em; |
||||||
|
} |
||||||
|
|
||||||
|
.vcd-screen-picker-grid input { |
||||||
|
appearance: none; |
||||||
|
cursor: pointer; |
||||||
|
} |
||||||
|
|
||||||
|
.vcd-screen-picker-selected img { |
||||||
|
border: 2px solid var(--brand-experiment); |
||||||
|
border-radius: 3px; |
||||||
|
} |
||||||
|
|
||||||
|
.vcd-screen-picker-grid label { |
||||||
|
overflow: hidden; |
||||||
|
padding: 4px 0px; |
||||||
|
cursor: pointer; |
||||||
|
} |
||||||
|
|
||||||
|
.vcd-screen-picker-grid label:hover { |
||||||
|
outline: 2px solid var(--brand-experiment); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
.vcd-screen-picker-grid div { |
||||||
|
white-space: nowrap; |
||||||
|
text-overflow: ellipsis; |
||||||
|
overflow: hidden; |
||||||
|
text-align: center; |
||||||
|
font-weight: 600; |
||||||
|
margin-inline: 0.5em; |
||||||
|
} |
||||||
|
|
||||||
|
.vcd-screen-picker-card { |
||||||
|
padding: 0.5em; |
||||||
|
box-sizing: border-box; |
||||||
|
} |
||||||
|
|
||||||
|
.vcd-screen-picker-preview img { |
||||||
|
width: 100%; |
||||||
|
margin-bottom: 0.5em; |
||||||
|
} |
||||||
|
|
||||||
|
.vcd-screen-picker-preview { |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
justify-content: center; |
||||||
|
align-items: center; |
||||||
|
margin-bottom: 1em; |
||||||
|
} |
||||||
|
|
||||||
|
.vcd-screen-picker-radio input { |
||||||
|
display: none; |
||||||
|
} |
||||||
|
|
||||||
|
.vcd-screen-picker-radio { |
||||||
|
background-color: var(--background-secondary); |
||||||
|
border: 1px solid var(--primary-800); |
||||||
|
padding: 0.3em; |
||||||
|
cursor: pointer; |
||||||
|
} |
||||||
|
|
||||||
|
.vcd-screen-picker-radio h2 { |
||||||
|
margin: 0; |
||||||
|
} |
||||||
|
|
||||||
|
.vcd-screen-picker-radio[data-checked="true"] { |
||||||
|
background-color: var(--brand-experiment); |
||||||
|
border-color: var(--brand-experiment); |
||||||
|
} |
||||||
|
|
||||||
|
.vcd-screen-picker-radio[data-checked="true"] h2 { |
||||||
|
color: var(--interactive-active); |
||||||
|
} |
||||||
|
|
||||||
|
.vcd-screen-picker-quality { |
||||||
|
display: flex; |
||||||
|
gap: 1em; |
||||||
|
|
||||||
|
margin-bottom: 0.5em; |
||||||
|
} |
||||||
|
|
||||||
|
.vcd-screen-picker-quality section { |
||||||
|
flex: 1 1 auto; |
||||||
|
} |
||||||
|
|
||||||
|
.vcd-screen-picker-radios { |
||||||
|
display: flex; |
||||||
|
width: 100%; |
||||||
|
border-radius: 3px; |
||||||
|
} |
||||||
|
|
||||||
|
.vcd-screen-picker-radios label { |
||||||
|
flex: 1 1 auto; |
||||||
|
text-align: center; |
||||||
|
} |
||||||
|
|
||||||
|
.vcd-screen-picker-radios label:first-child { |
||||||
|
border-radius: 3px 0 0 3px; |
||||||
|
} |
||||||
|
|
||||||
|
.vcd-screen-picker-radios label:last-child { |
||||||
|
border-radius: 0 3px 3px 0; |
||||||
|
} |
||||||
|
|
||||||
|
.vcd-screen-picker-audio { |
||||||
|
margin-bottom: 0; |
||||||
|
} |
||||||
|
|
||||||
|
.vcd-screen-picker-hint-description { |
||||||
|
color: var(--header-secondary); |
||||||
|
font-size: 14px; |
||||||
|
line-height: 20px; |
||||||
|
font-weight: 400; |
||||||
|
} |
Loading…
Reference in new issue