brutalism design

This commit is contained in:
2026-05-22 05:08:41 +06:00
parent e4ed785911
commit 1ca73e786c
9 changed files with 740 additions and 0 deletions

View File

@@ -0,0 +1,7 @@
{
"permissions": {
"allow": [
"Bash(python -c \"import sys,json; p=json.load\\(sys.stdin\\); deps={**p.get\\('dependencies',{}\\), **p.get\\('devDependencies',{}\\)}; [print\\(k,v\\) for k in sorted\\(deps\\) if any\\(x in k for x in ['mediasoup','socket','vueuse','vue']\\)]\")"
]
}
}

View File

@@ -0,0 +1,137 @@
import { useMediasoup } from '@shared/composables/use-mediasoup'
import { useSignaling } from '@shared/composables/use-signaling'
import { Device } from 'mediasoup-client'
import { markRaw, watch } from 'vue'
const ICE_SERVERS: RTCIceServer[] = [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:stun.l.google.com:5349' },
{ urls: 'stun:stun1.l.google.com:3478' },
{ urls: 'stun:stun1.l.google.com:5349' },
{ urls: 'stun:stun2.l.google.com:19302' },
{ urls: 'stun:stun2.l.google.com:5349' },
{ urls: 'stun:stun3.l.google.com:3478' },
{ urls: 'stun:stun3.l.google.com:5349' },
{ urls: 'stun:stun4.l.google.com:19302' },
{ urls: 'stun:stun4.l.google.com:5349' },
]
export default function () {
const { socket } = useSignaling()
const ms = useMediasoup()
watch(socket, (socket) => {
if (!socket)
return
socket.on('initialized', async (initData) => {
ms.device.value = new Device()
await ms.device.value.load({ routerRtpCapabilities: initData.rtpCapabilities })
const sendInfo = await socket.emitWithAck('create-transport', { producing: true, consuming: false })
const sendTransport = ms.device.value.createSendTransport({
...sendInfo,
iceServers: [...ICE_SERVERS, ...(sendInfo.iceServers ?? [])],
})
ms.sendTransport.value = sendTransport
await ms.transports.add({
id: sendTransport.id,
ref: markRaw(sendTransport),
})
sendTransport.on('connect', async ({ dtlsParameters }, callback, errback) => {
try {
await socket.emitWithAck('connect-transport', { transportId: sendTransport.id, dtlsParameters })
callback()
}
catch (error) {
if (error instanceof Error)
errback(error)
}
})
sendTransport.on('produce', async ({ kind, rtpParameters, appData }, callback, errback) => {
try {
const { id } = await socket.emitWithAck('produce', {
transportId: sendTransport.id,
kind,
rtpParameters,
appData,
})
callback({ id })
}
catch (error) {
if (error instanceof Error)
errback(error)
}
})
const recvInfo = await socket.emitWithAck('create-transport', { producing: false, consuming: true })
const recvTransport = ms.device.value.createRecvTransport({
...recvInfo,
iceServers: [...ICE_SERVERS, ...(recvInfo.iceServers ?? [])],
})
ms.recvTransport.value = recvTransport
await ms.transports.add({
id: recvTransport.id,
ref: markRaw(recvTransport),
})
recvTransport.on('connect', async ({ dtlsParameters }, callback, errback) => {
try {
await socket.emitWithAck('connect-transport', { transportId: recvTransport.id, dtlsParameters })
callback()
}
catch (error) {
if (error instanceof Error)
errback(error)
}
})
})
socket.on('new-consumer', async ({ id, producerId, kind, rtpParameters, socketId, appData, producerPaused }, cb) => {
const rt = ms.recvTransport.value
if (!rt)
return
const consumer = await rt.consume({
id,
producerId,
kind,
rtpParameters,
streamId: `${socketId}-${appData.source ?? 'stream'}`,
appData: { ...appData, socketId },
})
if (producerPaused)
consumer.pause()
await ms.consumers.add({
id: consumer.id,
paused: consumer.paused,
kind: consumer.kind,
appData: consumer.appData as Record<string, unknown>,
track: consumer.track,
ref: markRaw(consumer),
})
cb()
})
socket.on('consumer-closed', ({ consumerId }) => {
ms.consumers.get(consumerId)?.ref.close()
})
socket.on('consumer-paused', ({ consumerId }) => {
ms.consumers.get(consumerId)?.ref.pause()
})
socket.on('consumer-resumed', ({ consumerId }) => {
ms.consumers.get(consumerId)?.ref.resume()
})
socket.on('disconnect', () => {
ms.clearAll()
})
}, { immediate: true })
}

View File

@@ -0,0 +1,50 @@
import type { ChatMessage } from '@shared/api/generated-chad-api.ts'
import { useAuth } from '@shared/composables/use-auth.ts'
import { useClients } from '@shared/composables/use-clients'
import { useEventBus } from '@shared/composables/use-event-bus.ts'
import { useSignaling } from '@shared/composables/use-signaling'
import { watch, watchEffect } from 'vue'
export default function () {
const { authorized } = useAuth()
const signaling = useSignaling()
const { addClient, updateClient, removeClient, clear: clearClients } = useClients()
const eventBus = useEventBus()
watch(signaling.connected, (connected) => {
if (!connected)
return
const socket = signaling.socket.value!
socket.on('initialized', async ({ clients }) => {
addClient(...clients)
})
socket.on('new-client', (client) => {
addClient(client)
})
socket.on('client-switched-channel', (client) => {
updateClient(client.socketId, client)
})
socket.on('client-disconnected', (socketId) => {
removeClient(socketId)
})
socket.on('disconnect', () => {
clearClients()
})
socket.on('chat:new-message', (message: ChatMessage) => {
eventBus.emit('chat:new-message', message)
})
})
watchEffect(() => {
if (authorized.value) {
signaling.connect()
}
})
}

View File

@@ -0,0 +1,74 @@
import { createGlobalState } from '@vueuse/core'
import { computed, ref, watch } from 'vue'
import { useMediasoup } from './use-mediasoup'
import { useProducers } from './use-producers'
export const useMediaControls = createGlobalState(() => {
const { producers } = useMediasoup()
const { enableMic, enableVideo, disableVideo, enableShare, disableShare, pauseProducer, resumeProducer } = useProducers()
const micProducer = computed(() => producers.values.value.find(p => p.appData.source === 'mic'))
const cameraProducer = computed(() => producers.values.value.find(p => p.appData.source === 'camera'))
const shareProducer = computed(() => producers.values.value.find(p => p.appData.source === 'share'))
const isSoundEnabled = ref(true)
const isMicEnabled = computed(() => isSoundEnabled.value && !!micProducer.value && !micProducer.value.paused)
const isCameraEnabled = computed(() => !!cameraProducer.value && !cameraProducer.value.paused)
const isShareEnabled = computed(() => !!shareProducer.value)
watch(isSoundEnabled, async (enabled) => {
if (!enabled) {
if (micProducer.value && !micProducer.value.paused)
await pauseProducer(micProducer.value.id)
}
else {
if (micProducer.value?.paused)
await resumeProducer(micProducer.value.id)
}
})
async function toggleMic() {
if (!isSoundEnabled.value)
return
if (isMicEnabled.value) {
await pauseProducer(micProducer.value!.id)
}
else if (micProducer.value?.paused) {
await resumeProducer(micProducer.value.id)
}
else {
await enableMic()
}
}
function toggleSound() {
isSoundEnabled.value = !isSoundEnabled.value
}
async function toggleCamera() {
if (isCameraEnabled.value)
await disableVideo()
else
await enableVideo()
}
async function toggleShare() {
if (isShareEnabled.value)
await disableShare()
else
await enableShare()
}
return {
isMicEnabled,
isSoundEnabled,
isCameraEnabled,
isShareEnabled,
toggleMic,
toggleSound,
toggleCamera,
toggleShare,
}
})

View File

@@ -0,0 +1,119 @@
import type { Consumer, Device, Producer, Transport } from 'mediasoup-client/types'
import type { Ref } from 'vue'
import { createEventHook, createGlobalState } from '@vueuse/core'
import { computed, ref, shallowRef } from 'vue'
interface Snapshot<T> {
id: string
ref: T
[key: string]: any
}
export interface ProducerSnapshot extends Snapshot<Producer> {
paused: boolean
kind: string
appData: Record<string, unknown>
}
export interface ConsumerSnapshot extends Snapshot<Consumer> {
paused: boolean
kind: string
appData: Record<string, unknown>
track: MediaStreamTrack
}
export interface TransportSnapshot extends Snapshot<Transport> {
}
function createEntityStore<T extends { id: string }>() {
const store = ref(new Map<string, T>()) as unknown as Ref<Map<string, T>>
const onAddEvent = createEventHook<T>()
const onRemoveEvent = createEventHook<string>()
const values = computed(() => Array.from(store.value.values()))
async function add(entity: T) {
store.value.set(entity.id, entity)
// eslint-disable-next-line ts/ban-ts-comment
// @ts-expect-error
await onAddEvent.trigger(entity)
}
async function remove(id: string) {
store.value.delete(id)
await onRemoveEvent.trigger(id)
}
function get(id: string) {
return store.value.get(id)
}
function clear() {
store.value.clear()
}
return {
store,
values,
onAdd: onAddEvent.on,
onRemove: onRemoveEvent.on,
add,
remove,
get,
clear,
}
}
export const useMediasoup = createGlobalState(() => {
const device = shallowRef<Device>()
const sendTransport = shallowRef<Transport>()
const recvTransport = shallowRef<Transport>()
const producers = createEntityStore<ProducerSnapshot>()
producers.onAdd((producer) => {
producer.ref.observer.on('pause', () => {
producers.store.value.set(producer.id, { ...producer, paused: true })
})
producer.ref.observer.on('resume', () => {
producers.store.value.set(producer.id, { ...producer, paused: false })
})
producer.ref.observer.on('close', () => producers.remove(producer.id))
})
const consumers = createEntityStore<ConsumerSnapshot>()
consumers.onAdd((consumer) => {
consumer.ref.on('trackended', () => consumer.ref.close())
consumer.ref.observer.on('pause', () => {
consumers.store.value.set(consumer.id, { ...consumer, paused: true })
})
consumer.ref.observer.on('resume', () => {
consumers.store.value.set(consumer.id, { ...consumer, paused: false })
})
consumer.ref.observer.on('close', () => consumers.remove(consumer.id))
})
const transports = createEntityStore<TransportSnapshot>()
transports.onAdd((transport) => {
transport.ref.observer.on('close', () => transports.remove(transport.id))
})
function clearAll() {
device.value = undefined
sendTransport.value = undefined
recvTransport.value = undefined
producers.clear()
consumers.clear()
transports.clear()
}
return {
device,
sendTransport,
recvTransport,
producers,
consumers,
transports,
clearAll,
}
})

View File

@@ -0,0 +1,164 @@
import type { MediaKind, ProducerOptions } from 'mediasoup-client/types'
import { createGlobalState } from '@vueuse/core'
import { markRaw } from 'vue'
import { useMediasoup } from './use-mediasoup'
import { useSignaling } from './use-signaling'
export const useProducers = createGlobalState(() => {
const { socket } = useSignaling()
const { device, sendTransport, producers } = useMediasoup()
async function createProducer(options: ProducerOptions) {
if (!sendTransport.value || !device.value || !options.track)
return
if (!device.value.canProduce(options.track.kind as MediaKind))
return
const producer = await sendTransport.value.produce({ disableTrackOnPause: true, ...options })
producer.observer.on('trackended', () => disableProducer(producer.id))
await producers.add({
id: producer.id,
paused: producer.paused,
kind: producer.kind,
appData: producer.appData as Record<string, unknown>,
ref: markRaw(producer),
})
}
async function disableProducer(producerId: string) {
const snap = producers.get(producerId)
if (!snap)
return
try {
snap.ref.close()
await socket.value?.emitWithAck('close-producer', { producerId })
}
catch {}
}
async function pauseProducer(producerId: string) {
const snap = producers.get(producerId)
if (!snap || snap.paused)
return
try {
snap.ref.pause()
await socket.value?.emitWithAck('pause-producer', { producerId })
}
catch {
snap.ref.resume()
}
}
async function resumeProducer(producerId: string) {
const snap = producers.get(producerId)
if (!snap || !snap.paused)
return
try {
snap.ref.resume()
await socket.value?.emitWithAck('resume-producer', { producerId })
}
catch {
snap.ref.pause()
}
}
async function enableMic() {
for (const snap of producers.store.value.values()) {
if (snap.kind === 'audio')
return
}
const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
const track = stream.getAudioTracks()[0]
if (!track)
return
await createProducer({
track,
streamId: 'mic-video',
codecOptions: { opusStereo: true, opusDtx: true },
appData: { source: 'mic' },
})
}
async function disableMic() {
for (const snap of producers.store.value.values()) {
if (snap.kind === 'audio')
await disableProducer(snap.id)
}
}
async function enableVideo() {
for (const snap of producers.store.value.values()) {
if (snap.kind === 'video' && snap.appData.source !== 'share')
return
}
const stream = await navigator.mediaDevices.getUserMedia({
video: { width: { ideal: 1920 }, height: { ideal: 1080 }, frameRate: { ideal: 60 } },
})
const track = stream.getVideoTracks()[0]
if (!track)
return
await createProducer({
track,
streamId: 'mic-video',
appData: { source: 'camera' },
})
}
async function disableVideo() {
for (const snap of producers.store.value.values()) {
if (snap.kind === 'video' && snap.appData.source !== 'share')
await disableProducer(snap.id)
}
}
async function enableShare() {
for (const snap of producers.store.value.values()) {
if (snap.appData.source === 'share')
return
}
const stream = await navigator.mediaDevices.getDisplayMedia({
audio: false,
video: { displaySurface: 'monitor' },
})
const track = stream.getVideoTracks()[0]
if (!track)
return
await createProducer({
track,
streamId: 'share',
zeroRtpOnPause: true,
appData: { source: 'share' },
})
}
async function disableShare() {
for (const snap of producers.store.value.values()) {
if (snap.appData.source === 'share')
await disableProducer(snap.id)
}
}
return {
enableMic,
disableMic,
enableVideo,
disableVideo,
enableShare,
disableShare,
pauseProducer,
resumeProducer,
disableProducer,
}
})

View File

@@ -0,0 +1,101 @@
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
-- =====================
-- Channel
-- =====================
CREATE TABLE "new_Channel" (
"id" TEXT NOT NULL PRIMARY KEY,
"ownerId" TEXT,
"ownerUsername" TEXT,
"name" TEXT NOT NULL,
"persistent" BOOLEAN NOT NULL,
CONSTRAINT "Channel_ownerUsername_fkey" FOREIGN KEY ("ownerUsername") REFERENCES "User" ("username") ON DELETE CASCADE ON UPDATE CASCADE
);
INSERT INTO "new_Channel" ("id", "name", "ownerId", "ownerUsername", "persistent")
SELECT c."id", c."name", c."ownerId",
(SELECT u."username" FROM "User" u WHERE u."id" = c."ownerId"),
c."persistent"
FROM "Channel" c;
DROP TABLE "Channel";
ALTER TABLE "new_Channel" RENAME TO "Channel";
-- =====================
-- Message
-- =====================
CREATE TABLE "new_Message" (
"id" TEXT NOT NULL PRIMARY KEY,
"text" TEXT NOT NULL,
"senderId" TEXT,
"senderUsername" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "Message_senderUsername_fkey" FOREIGN KEY ("senderUsername") REFERENCES "User" ("username") ON DELETE SET NULL ON UPDATE CASCADE
);
INSERT INTO "new_Message" ("id", "text", "senderId", "senderUsername", "createdAt", "updatedAt")
SELECT m."id", m."text", m."senderId",
(SELECT u."username" FROM "User" u WHERE u."id" = m."senderId"),
m."createdAt", m."updatedAt"
FROM "Message" m;
DROP TABLE "Message";
ALTER TABLE "new_Message" RENAME TO "Message";
-- =====================
-- Session
-- =====================
CREATE TABLE "new_Session" (
"id" TEXT NOT NULL PRIMARY KEY,
"userId" TEXT NOT NULL,
"username" TEXT NOT NULL,
"expiresAt" DATETIME NOT NULL,
CONSTRAINT "Session_username_fkey" FOREIGN KEY ("username") REFERENCES "User" ("username") ON DELETE CASCADE ON UPDATE CASCADE
);
INSERT INTO "new_Session" ("id", "userId", "username", "expiresAt")
SELECT s."id", s."userId",
(SELECT u."username" FROM "User" u WHERE u."id" = s."userId"),
s."expiresAt"
FROM "Session" s;
DROP TABLE "Session";
ALTER TABLE "new_Session" RENAME TO "Session";
CREATE INDEX "Session_userId_idx" ON "Session"("userId");
-- =====================
-- User
-- =====================
CREATE TABLE "new_User" (
"id" TEXT NOT NULL,
"username" TEXT NOT NULL PRIMARY KEY,
"password" TEXT NOT NULL,
"displayName" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
INSERT INTO "new_User" ("createdAt", "displayName", "id", "password", "updatedAt", "username")
SELECT "createdAt", "displayName", "id", "password", "updatedAt", "username"
FROM "User";
DROP TABLE "User";
ALTER TABLE "new_User" RENAME TO "User";
CREATE UNIQUE INDEX "User_username_key" ON "User"("username");
-- =====================
-- UserPreferences
-- =====================
CREATE TABLE "new_UserPreferences" (
"userId" TEXT NOT NULL,
"username" TEXT NOT NULL PRIMARY KEY,
"toggleInputHotkey" TEXT DEFAULT '',
"toggleOutputHotkey" TEXT DEFAULT '',
CONSTRAINT "UserPreferences_username_fkey" FOREIGN KEY ("username") REFERENCES "User" ("username") ON DELETE CASCADE ON UPDATE CASCADE
);
INSERT INTO "new_UserPreferences" ("userId", "username", "toggleInputHotkey", "toggleOutputHotkey")
SELECT up."userId",
(SELECT u."username" FROM "User" u WHERE u."id" = up."userId"),
up."toggleInputHotkey", up."toggleOutputHotkey"
FROM "UserPreferences" up;
DROP TABLE "UserPreferences";
ALTER TABLE "new_UserPreferences" RENAME TO "UserPreferences";
CREATE UNIQUE INDEX "UserPreferences_userId_key" ON "UserPreferences"("userId");
CREATE UNIQUE INDEX "UserPreferences_username_key" ON "UserPreferences"("username");
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;

View File

@@ -0,0 +1,67 @@
/*
Warnings:
- You are about to drop the column `ownerId` on the `Channel` table. All the data in the column will be lost.
- You are about to drop the column `senderId` on the `Message` table. All the data in the column will be lost.
- You are about to drop the column `userId` on the `Session` table. All the data in the column will be lost.
- You are about to drop the column `id` on the `User` table. All the data in the column will be lost.
- You are about to drop the column `userId` on the `UserPreferences` table. All the data in the column will be lost.
*/
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Channel" (
"id" TEXT NOT NULL PRIMARY KEY,
"ownerUsername" TEXT,
"name" TEXT NOT NULL,
"persistent" BOOLEAN NOT NULL,
CONSTRAINT "Channel_ownerUsername_fkey" FOREIGN KEY ("ownerUsername") REFERENCES "User" ("username") ON DELETE CASCADE ON UPDATE CASCADE
);
INSERT INTO "new_Channel" ("id", "name", "ownerUsername", "persistent") SELECT "id", "name", "ownerUsername", "persistent" FROM "Channel";
DROP TABLE "Channel";
ALTER TABLE "new_Channel" RENAME TO "Channel";
CREATE TABLE "new_Message" (
"id" TEXT NOT NULL PRIMARY KEY,
"text" TEXT NOT NULL,
"senderUsername" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "Message_senderUsername_fkey" FOREIGN KEY ("senderUsername") REFERENCES "User" ("username") ON DELETE SET NULL ON UPDATE CASCADE
);
INSERT INTO "new_Message" ("createdAt", "id", "senderUsername", "text", "updatedAt") SELECT "createdAt", "id", "senderUsername", "text", "updatedAt" FROM "Message";
DROP TABLE "Message";
ALTER TABLE "new_Message" RENAME TO "Message";
CREATE TABLE "new_Session" (
"id" TEXT NOT NULL PRIMARY KEY,
"username" TEXT NOT NULL,
"expiresAt" DATETIME NOT NULL,
CONSTRAINT "Session_username_fkey" FOREIGN KEY ("username") REFERENCES "User" ("username") ON DELETE CASCADE ON UPDATE CASCADE
);
INSERT INTO "new_Session" ("expiresAt", "id", "username") SELECT "expiresAt", "id", "username" FROM "Session";
DROP TABLE "Session";
ALTER TABLE "new_Session" RENAME TO "Session";
CREATE INDEX "Session_username_idx" ON "Session"("username");
CREATE TABLE "new_User" (
"username" TEXT NOT NULL PRIMARY KEY,
"password" TEXT NOT NULL,
"displayName" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
INSERT INTO "new_User" ("createdAt", "displayName", "password", "updatedAt", "username") SELECT "createdAt", "displayName", "password", "updatedAt", "username" FROM "User";
DROP TABLE "User";
ALTER TABLE "new_User" RENAME TO "User";
CREATE UNIQUE INDEX "User_username_key" ON "User"("username");
CREATE TABLE "new_UserPreferences" (
"username" TEXT NOT NULL PRIMARY KEY,
"toggleInputHotkey" TEXT DEFAULT '',
"toggleOutputHotkey" TEXT DEFAULT '',
CONSTRAINT "UserPreferences_username_fkey" FOREIGN KEY ("username") REFERENCES "User" ("username") ON DELETE CASCADE ON UPDATE CASCADE
);
INSERT INTO "new_UserPreferences" ("toggleInputHotkey", "toggleOutputHotkey", "username") SELECT "toggleInputHotkey", "toggleOutputHotkey", "username" FROM "UserPreferences";
DROP TABLE "UserPreferences";
ALTER TABLE "new_UserPreferences" RENAME TO "UserPreferences";
CREATE UNIQUE INDEX "UserPreferences_username_key" ON "UserPreferences"("username");
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;

View File

@@ -0,0 +1,21 @@
/*
Warnings:
- You are about to drop the column `username` on the `Session` table. All the data in the column will be lost.
- Added the required column `userId` to the `Session` table without a default value. This is not possible if the table is not empty.
*/
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Session" (
"id" TEXT NOT NULL PRIMARY KEY,
"userId" TEXT NOT NULL,
"expiresAt" DATETIME NOT NULL,
CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("username") ON DELETE CASCADE ON UPDATE CASCADE
);
INSERT INTO "new_Session" ("expiresAt", "id") SELECT "expiresAt", "id" FROM "Session";
DROP TABLE "Session";
ALTER TABLE "new_Session" RENAME TO "Session";
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;