移除废弃文件
This commit is contained in:
parent
4519a6701e
commit
8b2fd539b4
@ -1,119 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>Cloud Upload Icon</title>
|
|
||||||
<style>
|
|
||||||
#box {
|
|
||||||
width: 20rem;
|
|
||||||
height: 5rem;
|
|
||||||
backdrop-filter: blur(5px);
|
|
||||||
background-color: rgb(252, 252, 252);
|
|
||||||
border-radius: 1em;
|
|
||||||
border: 1px solid rgb(241, 241, 241);
|
|
||||||
position: relative;
|
|
||||||
user-select: none;
|
|
||||||
cursor: pointer;
|
|
||||||
margin: 1rem 2rem;
|
|
||||||
}
|
|
||||||
#box:hover > #drop {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
#cloud {
|
|
||||||
width: 82px;
|
|
||||||
height: 30px;
|
|
||||||
background: #e7f4fd;
|
|
||||||
background: -webkit-linear-gradient(top,#e7f4fd 5%,#ceedfd 100%);
|
|
||||||
border-radius: 25px;
|
|
||||||
position: relative;
|
|
||||||
margin: 33px auto 5px;
|
|
||||||
}
|
|
||||||
#cloud:before {
|
|
||||||
width: 45px;
|
|
||||||
height: 45px;
|
|
||||||
top: -22px;
|
|
||||||
right: 12px;
|
|
||||||
border-radius: 50px;
|
|
||||||
}
|
|
||||||
#cloud:after {
|
|
||||||
width: 25px;
|
|
||||||
height: 25px;
|
|
||||||
top: -12px;
|
|
||||||
left: 12px;
|
|
||||||
border-radius: 25px;
|
|
||||||
}
|
|
||||||
#cloud:before,
|
|
||||||
#cloud:after {
|
|
||||||
content: "";
|
|
||||||
background: #e7f4fd;
|
|
||||||
position: absolute;
|
|
||||||
z-index: -1;
|
|
||||||
}
|
|
||||||
#cloud span {
|
|
||||||
width: 87px;
|
|
||||||
position: absolute;
|
|
||||||
bottom: -2px;
|
|
||||||
background: #000;
|
|
||||||
z-index: -1;
|
|
||||||
box-shadow: 0 0 6px 2px rgba(0,0,0,0.1);
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#drop {
|
|
||||||
width: 100%;
|
|
||||||
color: #999;
|
|
||||||
line-height: 5rem;
|
|
||||||
text-align: center;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<script>
|
|
||||||
onload = () => {
|
|
||||||
const file = document.createElement('input')
|
|
||||||
file.type = 'file'
|
|
||||||
file.onchange = event => {
|
|
||||||
for (const i of file.files) {
|
|
||||||
console.log('已获取文件:', i)
|
|
||||||
}
|
|
||||||
file.value = null
|
|
||||||
}
|
|
||||||
|
|
||||||
box.onclick = () => file.click()
|
|
||||||
|
|
||||||
let dragStats = 0
|
|
||||||
document.ondragover = e => {
|
|
||||||
e.preventDefault()
|
|
||||||
if(dragStats) return
|
|
||||||
drop.style.display = 'block'
|
|
||||||
}
|
|
||||||
|
|
||||||
document.ondragleave = e => {
|
|
||||||
e.preventDefault()
|
|
||||||
if(dragStats) return
|
|
||||||
dragStats = setTimeout(() => {
|
|
||||||
dragStats = 0
|
|
||||||
drop.style.display = ''
|
|
||||||
}, 1000)
|
|
||||||
}
|
|
||||||
|
|
||||||
document.ondrop = e => {
|
|
||||||
e.preventDefault()
|
|
||||||
dragStats = 0
|
|
||||||
drop.style.display = ''
|
|
||||||
const files = e.dataTransfer.files
|
|
||||||
for (const i of files) {
|
|
||||||
console.log('已获取文件:', i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="box">
|
|
||||||
<div id="cloud"><span></span></div>
|
|
||||||
<span id="drop">点击或拖拽音乐到此处共享您的音乐</span>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
36
public/cs.js
36
public/cs.js
@ -1,36 +0,0 @@
|
|||||||
// 动态分片流
|
|
||||||
// 1. 默认提供分片流
|
|
||||||
// 2. 允许请求者指定数据段, 返回相应分片, 发送队列可以并行, 接收队列在检查没有新数据时, 补充要求失败的分片
|
|
||||||
// 3. 可靠模式下分片是完整的, 但响应不是立即的, 当请求超时时放弃接受和发送队列中的分片
|
|
||||||
// 4. 同一个资源可以向不同的提供者请求, 将不同区段的分片合并然后存储
|
|
||||||
// 5. 不需要验证hash, 但是需要验证顺序, 发送者应当标记这个分片的开始和结束位置
|
|
||||||
|
|
||||||
// 资源id, 开始位置, 结束位置, 数据
|
|
||||||
// 资源的其他信息, 如hash, 大小, 类型等, 由其他方式获取
|
|
||||||
// 并不信任发送者, 所以必须验证发送者的分片HASH, 所以资源信息里应当包含每个分片的HASH
|
|
||||||
// 但如果每个HASH都要验证, 那么验证的成本太高, 所以应当有一个HASH验证的阈值, 例如每100个分片验证一次, 如果验证失败, 则放弃这个分片, 重新请求
|
|
||||||
|
|
||||||
// 数据块, 每个数据块验证一次HASH, 如果验证失败, 则放弃这个数据块, 重新请求
|
|
||||||
// 数据块的大小应当是2的幂, 例如2, 4, 8, 16, 32, 64, 128, 256, 512, 1024等
|
|
||||||
// 数据块的大小应当是分片的倍数, 例如分片大小是1024, 那么数据块大小应当是1024的倍数, 例如1024, 2048, 4096等
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {
|
|
||||||
* id: 'resource id',
|
|
||||||
* type: 'resource type',
|
|
||||||
* size: 'resource size',
|
|
||||||
* hash: 'resource hash',
|
|
||||||
* blocks: [
|
|
||||||
* 'md5 hash of block 1',
|
|
||||||
* 'md5 hash of block 2',
|
|
||||||
* 'md5 hash of block 3',
|
|
||||||
* ]
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
|
|
||||||
// 分片交叉验证
|
|
||||||
// 1. 每个分片都没有hash
|
|
||||||
// 2. 文件有一个完整的hash
|
|
||||||
// 3. 文件取4个交叉验证维度
|
|
||||||
// 4. 16个HASH验证1GB文件
|
|
||||||
// 5. 且只在完整hash验证失败时才验证分片
|
|
@ -1,192 +0,0 @@
|
|||||||
export default class Entanglement {
|
|
||||||
constructor({ options }) {
|
|
||||||
this.event = {}
|
|
||||||
this.options = options
|
|
||||||
this.store = {}
|
|
||||||
this.users = []
|
|
||||||
this.channels = [{ name: 'json' }]
|
|
||||||
this.ws = this.__create_websocket()
|
|
||||||
}
|
|
||||||
|
|
||||||
async __create_webrtc(user) {
|
|
||||||
const pc = new RTCPeerConnection(this.options)
|
|
||||||
// 当有新的媒体流加入时触发
|
|
||||||
pc.onicecandidate = (event) => {
|
|
||||||
if (event.candidate) {
|
|
||||||
console.debug(user.name, '发出 candidate 候选通道')
|
|
||||||
this.ws.send(JSON.stringify({ type: 'candidate', user, candidate: event.candidate }))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 当连接状态发生改变时触发
|
|
||||||
pc.oniceconnectionstatechange = (event) => {
|
|
||||||
if (pc.iceConnectionState === 'disconnected' || pc.iceConnectionState === 'failed') {
|
|
||||||
console.error(user.name, '需要添加新的 candidate')
|
|
||||||
} else if (pc.iceConnectionState === 'connected' || pc.iceConnectionState === 'completed') {
|
|
||||||
console.debug(user.name, 'WebRTC 连接已经建立成功')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 协商新的会话, 建立初始连接或在网络条件发生变化后重新协商连接
|
|
||||||
pc.onnegotiationneeded = async (event) => {
|
|
||||||
console.log('onnegotiationneeded', event)
|
|
||||||
//const offer = await pc.createOffer()
|
|
||||||
//await pc.setLocalDescription(offer)
|
|
||||||
//this.ws.send(JSON.stringify({ type: 'offer', user, offer }))
|
|
||||||
}
|
|
||||||
// 当有新的媒体流加入时触发
|
|
||||||
pc.ontrack = (event) => {
|
|
||||||
console.log('ontrack', event)
|
|
||||||
}
|
|
||||||
// 当有新的数据通道加入时触发
|
|
||||||
pc.ondatachannel = event => {
|
|
||||||
console.log(user.name, '建立', event.channel.label, '接收通道')
|
|
||||||
event.channel.onmessage = event => {
|
|
||||||
console.log(user.name, '发来', event.target.label, '通道消息', event.data)
|
|
||||||
}
|
|
||||||
event.channel.onopen = () => {
|
|
||||||
console.log(user.name, '打开', event.channel.label, '接收通道')
|
|
||||||
//event.channel.send(JSON.stringify({ name: 'sato', hello: 'world' }))
|
|
||||||
}
|
|
||||||
event.channel.onclose = () => {
|
|
||||||
console.log(user.name, '关闭', event.channel.label, '接收通道')
|
|
||||||
}
|
|
||||||
event.channel.onerror = () => {
|
|
||||||
console.log(user.name, '通道', event.channel.label, '发生错误')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 创建数据通道
|
|
||||||
user.channels = this.channels.map(item => {
|
|
||||||
const channel = pc.createDataChannel(item.name, { reliable: true })
|
|
||||||
channel.onopen = () => {
|
|
||||||
console.log('打开数据发送通道')
|
|
||||||
channel.send(JSON.stringify({ name: 'sato', hello: 'world' }))
|
|
||||||
}
|
|
||||||
channel.onmessage = event => {
|
|
||||||
console.log('收到数据发送通道消息', event.data)
|
|
||||||
}
|
|
||||||
channel.onclose = () => {
|
|
||||||
console.log('关闭数据发送通道')
|
|
||||||
}
|
|
||||||
channel.onerror = () => {
|
|
||||||
console.log('发送通道发生错误')
|
|
||||||
}
|
|
||||||
return { channel, ...item }
|
|
||||||
})
|
|
||||||
return pc
|
|
||||||
}
|
|
||||||
|
|
||||||
async __create_websocket() {
|
|
||||||
const host = window.location.host
|
|
||||||
const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws'
|
|
||||||
const ws = new WebSocket(`${protocol}://${host}/entanglement?name=sato&channel=chat`)
|
|
||||||
ws.onopen = async () => {
|
|
||||||
console.log('websocket 连接成功')
|
|
||||||
if (this.ws instanceof Promise) {
|
|
||||||
this.ws = await this.ws
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ws.onclose = async () => {
|
|
||||||
console.log('websocket 连接关闭, 3s后尝试重新连接...')
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 3000))
|
|
||||||
this.ws = await this.__create_websocket()
|
|
||||||
}
|
|
||||||
ws.onerror = async () => {
|
|
||||||
console.log('websocket 连接错误, 3s后尝试重新连接...')
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 3000))
|
|
||||||
this.ws = await this.__create_websocket()
|
|
||||||
}
|
|
||||||
ws.onmessage = async event => {
|
|
||||||
const data = JSON.parse(event.data)
|
|
||||||
if (data.type === 'list') {
|
|
||||||
console.debug('收到在线列表', data.list)
|
|
||||||
await Promise.all(data.list.map(async user => {
|
|
||||||
console.debug('发送给', user.name, 'offer')
|
|
||||||
const pc = await this.__create_webrtc(user)
|
|
||||||
const offer = await pc.createOffer()
|
|
||||||
await pc.setLocalDescription(offer)
|
|
||||||
this.users.push({ ...user, webrtc: pc }) // 必须在send之前存入
|
|
||||||
this.ws.send(JSON.stringify({ type: 'offer', user, offer }))
|
|
||||||
}))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (data.type === 'push') {
|
|
||||||
console.debug(data.user.name, '上线', data)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (data.type === 'pull') {
|
|
||||||
console.debug(data.user.name, '下线', data)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (data.type === 'offer') {
|
|
||||||
console.debug(data.user.name, '发来 offer')
|
|
||||||
const pc = await this.__create_webrtc(data.user)
|
|
||||||
await pc.setRemoteDescription(data.offer)
|
|
||||||
const answer = await pc.createAnswer()
|
|
||||||
await pc.setLocalDescription(answer)
|
|
||||||
this.ws.send(JSON.stringify({ type: 'answer', user: data.user, answer }))
|
|
||||||
this.users.push({ ...data.user, webrtc: pc })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (data.type === 'answer') {
|
|
||||||
console.debug(data.user.name, '发来 answer')
|
|
||||||
const pc = this.users.find(user => user.id === data.user.id).webrtc
|
|
||||||
await pc.setRemoteDescription(data.answer)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (data.type === 'candidate') {
|
|
||||||
console.debug(data.user.name, '发来 candidate 候选通道', JSON.stringify(this.users))
|
|
||||||
const pc = this.users.find(user => user.id === data.user.id).webrtc
|
|
||||||
await pc.addIceCandidate(data.candidate)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
console.error('收到未知数据:', data)
|
|
||||||
}
|
|
||||||
return ws
|
|
||||||
}
|
|
||||||
|
|
||||||
// 向所有在线的用户广播消息(webrtc)
|
|
||||||
send_all(channel_name, data) {
|
|
||||||
console.log('向', channel_name, '频道广播消息:', data)
|
|
||||||
console.log('在线用户:', this.users)
|
|
||||||
this.users.forEach(async user => {
|
|
||||||
console.log('向', user.name, '发送', channel_name, '频道消息:', data)
|
|
||||||
const ch = user.channels.find(item => item.name === channel_name).channel
|
|
||||||
// 等待 datachannel 打开(临时解决方案)
|
|
||||||
while (ch.readyState !== 'open') {
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 100))
|
|
||||||
}
|
|
||||||
console.log('完成发送', channel_name, '频道消息:', data)
|
|
||||||
ch.send(JSON.stringify(data))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 数据被修改时触发
|
|
||||||
set(name, data) {
|
|
||||||
// 递归创建代理对象
|
|
||||||
const useProxy = (obj, path = []) => {
|
|
||||||
const proxy = new Proxy(obj, {
|
|
||||||
set: (target, key, value) => {
|
|
||||||
if (!Array.isArray(target) || key !== 'length') {
|
|
||||||
console.log('对象被修改', [...path, key], value)
|
|
||||||
this.send_all(name, { key: [...path, key], value }) // 向所有在线的用户广播消息
|
|
||||||
}
|
|
||||||
return Reflect.set(target, key, value)
|
|
||||||
},
|
|
||||||
get: (target, key) => {
|
|
||||||
return target[key]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
Object.keys(obj).forEach(key => {
|
|
||||||
if (typeof obj[key] === 'object') {
|
|
||||||
obj[key] = useProxy(obj[key], [...path, key])
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return proxy
|
|
||||||
}
|
|
||||||
return Reflect.set(this.store, name, useProxy(data))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 读取一个通道
|
|
||||||
get(name) {
|
|
||||||
return this.store[name]
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,94 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="zh">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>webRTC</title>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div>
|
|
||||||
<h1>Entanglement</h1>
|
|
||||||
<p>同步</p>
|
|
||||||
</div>
|
|
||||||
<script type="module">
|
|
||||||
import { List, ListItem, useProxy } from './weigets.js'
|
|
||||||
//import Entanglement from './entanglement.js'
|
|
||||||
//const entanglement = new Entanglement({
|
|
||||||
// options: {
|
|
||||||
// iceServers: [
|
|
||||||
// {
|
|
||||||
// urls: 'turn:satori.love:3478?transport=udp',
|
|
||||||
// username: 'x-username',
|
|
||||||
// credential: 'x-password'
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// urls: [
|
|
||||||
// 'stun:stun.1und1.de',
|
|
||||||
// 'stun:stun.callwithus.com',
|
|
||||||
// 'stun:stun.ekiga.net',
|
|
||||||
// 'stun:stun.fwdnet.net',
|
|
||||||
// 'stun:stun.fwdnet.net:3478',
|
|
||||||
// 'stun:stun.gmx.net',
|
|
||||||
// 'stun:stun.iptel.org',
|
|
||||||
// 'stun:stun.internetcalls.com',
|
|
||||||
// 'stun:stun.minisipserver.com',
|
|
||||||
// 'stun:stun.schlund.de',
|
|
||||||
// 'stun:stun.sipgate.net',
|
|
||||||
// 'stun:stun.sipgate.net:10000',
|
|
||||||
// 'stun:stun.softjoys.com',
|
|
||||||
// 'stun:stun.softjoys.com:3478',
|
|
||||||
// 'stun:stun.voip.aebc.com',
|
|
||||||
// 'stun:stun.voipbuster.com',
|
|
||||||
// 'stun:stun.voipstunt.com',
|
|
||||||
// 'stun:stun.voxgratia.org',
|
|
||||||
// 'stun:stun.wirlab.net',
|
|
||||||
// 'stun:stun.xten.com',
|
|
||||||
// 'stun:stunserver.org',
|
|
||||||
// 'stun:stun01.sipphone.com',
|
|
||||||
// 'stun:stun.zoiper.com'
|
|
||||||
// ]
|
|
||||||
// }
|
|
||||||
// ],
|
|
||||||
// iceCandidatePoolSize: 10, // 限制 ICE 候选者的数量
|
|
||||||
// iceTransportPolicy: 'all', // 使用所有可用的候选者
|
|
||||||
// bundlePolicy: 'balanced', // 每種類型的內容建立一個單獨的傳輸
|
|
||||||
// }, server: {}
|
|
||||||
//})
|
|
||||||
//entanglement.set('json', {
|
|
||||||
// users: { name: 'users', list: [{ name: 'satori' }] },
|
|
||||||
// music: { name: 'music', list: [{ name: 'satori' }] },
|
|
||||||
// image: { name: 'image', list: [{ name: 'satori' }] }
|
|
||||||
//})
|
|
||||||
//await new Promise(resolve => setTimeout(resolve, 3000))
|
|
||||||
const store = useProxy({
|
|
||||||
users: { name: 'users', list: [{ name: 'satori' },{ name: 'sato' }] },
|
|
||||||
music: { name: 'music', list: [{ name: 'satori' },{ name: 'sato' }] },
|
|
||||||
image: { name: 'image', list: [{ name: 'satori' },{ name: 'sato' }] },
|
|
||||||
}, data => {
|
|
||||||
//console.log('触发了:', data.key, data.value)
|
|
||||||
console.log('当前数据:', store)
|
|
||||||
})
|
|
||||||
//console.log('当前数据:', JSON.stringify(store))
|
|
||||||
delete store.users.list[1]
|
|
||||||
//console.log('当前数据:', JSON.stringify(store))
|
|
||||||
store.users.list.push({ name: 'koishi' })
|
|
||||||
//store.users.list.push({ name: 'koishi' })
|
|
||||||
//store.users.list.push({ name: 'koishi' })
|
|
||||||
//console.log('当前数据:', JSON.stringify(store))
|
|
||||||
//document.body.appendChild(List({
|
|
||||||
// children: store.users.list.map(user => ListItem({ textContent: user.name })),
|
|
||||||
// onclick: () => {
|
|
||||||
// if (event.target.tagName === 'LI') {
|
|
||||||
// console.log('点击了:', event.target.textContent)
|
|
||||||
// }
|
|
||||||
// //store.users.name = 'koishi'
|
|
||||||
// store.users.list.push({ name: 'koishi' })
|
|
||||||
// //store.users.list.pop()
|
|
||||||
// // 在数据从其他位置被删除时也能删除这里的元素
|
|
||||||
// }
|
|
||||||
//}))
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
97
style.css
97
style.css
@ -1,97 +0,0 @@
|
|||||||
:root {
|
|
||||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
|
||||||
line-height: 1.5;
|
|
||||||
font-weight: 400;
|
|
||||||
|
|
||||||
color-scheme: light dark;
|
|
||||||
color: rgba(255, 255, 255, 0.87);
|
|
||||||
background-color: #242424;
|
|
||||||
|
|
||||||
font-synthesis: none;
|
|
||||||
text-rendering: optimizeLegibility;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
-webkit-text-size-adjust: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
font-weight: 500;
|
|
||||||
color: #646cff;
|
|
||||||
text-decoration: inherit;
|
|
||||||
}
|
|
||||||
a:hover {
|
|
||||||
color: #535bf2;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
display: flex;
|
|
||||||
place-items: center;
|
|
||||||
min-width: 320px;
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 3.2em;
|
|
||||||
line-height: 1.1;
|
|
||||||
}
|
|
||||||
|
|
||||||
#app {
|
|
||||||
max-width: 1280px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 2rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
height: 6em;
|
|
||||||
padding: 1.5em;
|
|
||||||
will-change: filter;
|
|
||||||
transition: filter 300ms;
|
|
||||||
}
|
|
||||||
.logo:hover {
|
|
||||||
filter: drop-shadow(0 0 2em #646cffaa);
|
|
||||||
}
|
|
||||||
.logo.vanilla:hover {
|
|
||||||
filter: drop-shadow(0 0 2em #f7df1eaa);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
padding: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.read-the-docs {
|
|
||||||
color: #888;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
padding: 0.6em 1.2em;
|
|
||||||
font-size: 1em;
|
|
||||||
font-weight: 500;
|
|
||||||
font-family: inherit;
|
|
||||||
background-color: #1a1a1a;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: border-color 0.25s;
|
|
||||||
}
|
|
||||||
button:hover {
|
|
||||||
border-color: #646cff;
|
|
||||||
}
|
|
||||||
button:focus,
|
|
||||||
button:focus-visible {
|
|
||||||
outline: 4px auto -webkit-focus-ring-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: light) {
|
|
||||||
:root {
|
|
||||||
color: #213547;
|
|
||||||
background-color: #ffffff;
|
|
||||||
}
|
|
||||||
a:hover {
|
|
||||||
color: #747bff;
|
|
||||||
}
|
|
||||||
button {
|
|
||||||
background-color: #f9f9f9;
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user