base commit

This commit is contained in:
Yeray 2024-05-19 17:20:32 +02:00 committed by Yeray Catala
commit 9c5fb97cdc
13 changed files with 7993 additions and 0 deletions

24
.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# build output
dist/
# generated types
.astro/
# dependencies
node_modules/
# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# environment variables
.env
.env.production
# macOS-specific files
.DS_Store
# jetbrains setting folder
.idea/

4
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,4 @@
{
"recommendations": ["astro-build.astro-vscode"],
"unwantedRecommendations": []
}

11
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,11 @@
{
"version": "0.2.0",
"configurations": [
{
"command": "./node_modules/.bin/astro dev",
"name": "Development server",
"request": "launch",
"type": "node-terminal"
}
]
}

5
README.md Normal file
View File

@ -0,0 +1,5 @@
# Cypher23
Cypher23 is a fictional story about a distopic future of totalitarian goverment by states and big tech corporations.
The site emulates a shell with a single tool available, `time-radio`, allowing to listen to audio streams from different ages.

8
astro.config.mjs Normal file
View File

@ -0,0 +1,8 @@
import { defineConfig } from 'astro/config';
import vue from "@astrojs/vue";
// https://astro.build/config
export default defineConfig({
integrations: [vue()]
});

7599
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

19
package.json Normal file
View File

@ -0,0 +1,19 @@
{
"name": "cypher23",
"type": "module",
"version": "0.0.1",
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"build": "astro check && astro build",
"preview": "astro preview",
"astro": "astro"
},
"dependencies": {
"@astrojs/check": "^0.7.0",
"@astrojs/vue": "^4.2.0",
"astro": "^4.8.6",
"typescript": "^5.4.5",
"vue": "^3.4.27"
}
}

BIN
public/cypher23.mp3 Normal file

Binary file not shown.

BIN
public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

244
src/components/Prompt.vue Normal file
View File

@ -0,0 +1,244 @@
<script setup lang="ts">
import { ref, onMounted, watch } from 'vue'
const input = ref('time-radio fut://cypher23@2668037535473')
const history = ref<{ command: string, response: string, when: number }[]>([]);
const timeIndex = ref(0)
const promptLocked = ref(false)
const audio = ref(new Audio('/cypher23.mp3'))
const updateInterval = ref(0)
const currentCommand = ref(-1) // -1 is new command, 0 is the last command, 1 is the command before the last command
const textarea = ref<HTMLTextAreaElement | null>(null)
onMounted(() => {
autofocus()
document.querySelector('html')?.addEventListener('click', () => autofocus())
document.querySelector('html')?.addEventListener('keypress', (event: KeyboardEvent) => handleKeys(event))
document.querySelector('html')?.addEventListener('keydown', (event: KeyboardEvent) => {
if (event.key === 'ArrowUp' && promptLocked.value === false) {
navigatePreviousCommand()
} else if (event.key === 'ArrowDown' && promptLocked.value === false) {
navigateNextCommand()
} else {
autofocus()
}
})
})
watch(input, () => {
autofocus()
})
function handleKeys(event: KeyboardEvent) {
if (event.key === 's' && promptLocked.value === true && audio.value.paused === false) {
event.preventDefault()
clearInterval(updateInterval.value)
timeIndex.value = 0
audio.value.pause()
audio.value.currentTime = 0
unlockPrompt()
} else if (event.key !== 'Enter') {
// do nothing, textarea handles it
return
} else {
event.preventDefault()
runCommand()
}
}
function runCommand() {
if (input.value === 'time-radio fut://cypher23@2668037535473') {
playRadio()
} else if (input.value.includes('time-radio past://')) {
pastNotImplemented()
} else if (input.value === 'time-radio --help') {
timeRadioHelpCommand()
} else if (input.value.includes('time-radio')) {
streamNotFound()
} else if (input.value === 'help') {
helpCommand()
} else if (input.value === "") {
history.value.push({ command: input.value, response: '', when: Date.now() })
} else {
commandNotFound()
}
autofocus()
currentCommand.value = -1
}
function timeIndexToString(timeIndex: number) {
const timeIndexString = timeIndex.toString()
if (timeIndex < 60) {
return '00:' + (timeIndexString.length === 1 ? '0' + timeIndexString : timeIndexString)
} else {
const minutes = Math.floor(timeIndex / 60)
const seconds = timeIndex % 60
return (minutes < 10 ? '0' + minutes : minutes) + ':' + (seconds < 10 ? '0' + seconds : seconds)
}
}
function playRadio() {
const timeSpaceStreamDate = new Date(2668037535473)
const response =
`time-radio: Playing space-time audio stream cypher23 recorded at ${timeSpaceStreamDate}\nPlaying... ${timeIndexToString(timeIndex.value)} / 3:35 | Press s to stop the stream.`
const commandHistoryEntry = { command: input.value, response, when: Date.now() }
history.value.push(commandHistoryEntry)
input.value = ''
lockPrompt()
audio.value.play()
updateInterval.value = setInterval(() => {
timeIndex.value++
const response =
`time-radio: Playing space-time audio stream cypher23 recorded at ${timeSpaceStreamDate}\nPlaying... ${timeIndexToString(timeIndex.value)} / 3:35 | Press s to stop the stream.`
history.value
.filter(historyEntry => historyEntry.when === commandHistoryEntry.when)
.forEach(historyEntry => historyEntry.response = response)
}, 1000)
setTimeout(() => {
history.value
.filter(historyEntry => historyEntry.when === commandHistoryEntry.when)
.forEach(historyEntry => historyEntry.response = 'time-radio: Finished playing space-time audio stream cypher23 recorded at ' + timeSpaceStreamDate)
clearInterval(updateInterval.value)
unlockPrompt()
autofocus()
timeIndex.value = 0
audio.value.pause()
audio.value.currentTime = 0
}, 215000)
}
function pastNotImplemented() {
const streamUrl = input.value.split(' ')[1]
history.value.push({ command: input.value, response: 'time-radio: (160c53ea) Past streams are not implemented yet, you could try listening to podcasts tho. Cannot resolve ' + streamUrl, when: Date.now() })
input.value = ''
}
function streamNotFound() {
const streamUrl = input.value.split(' ')[1]
history.value.push({ command: input.value, response: 'time-radio: (d2ef1a03) Cound not resolve space-time audio stream: ' + streamUrl, when: Date.now() })
input.value = ''
}
function commandNotFound() {
history.value.push({ command: input.value, response: input.value.split(" ")[0] + ': Command not found.', when: Date.now() })
input.value = ''
}
function lockPrompt() {
promptLocked.value = true
}
function unlockPrompt() {
promptLocked.value = false
autofocus()
}
function autofocus() {
textarea.value!.blur()
textarea.value!.focus()
textarea.value!.selectionStart = textarea.value!.selectionEnd = textarea.value!.value.length
}
function navigatePreviousCommand() {
const filteredHistory = history.value.filter(c => c.response)
if (currentCommand.value >= filteredHistory.length - 1) return
currentCommand.value++
const previousCommand = filteredHistory.sort((c, c2) => c2.when - c.when).filter(c => c.response)[currentCommand.value]
input.value = previousCommand.command
autofocus()
}
function navigateNextCommand() {
if (currentCommand.value <= 0) {
currentCommand.value = -1
input.value = ''
return
}
const filteredHistory = history.value.filter(c => c.response)
currentCommand.value--
const nextCommand = filteredHistory.sort((c, c2) => c2.when - c.when).filter(c => c.response)[currentCommand.value]
input.value = nextCommand.command
autofocus()
}
function timeRadioHelpCommand() {
history.value.push({ command: input.value, response: 'Time radio allows to share audio streams with past and future peers through space-time. Use with caution.\nThis is the client only version, you can listen but not record streams.\nUsage: time-radio <past | fut>://<streamId>@<timestamp>', when: Date.now() })
input.value = ''
}
function helpCommand() {
const helpResponse = `Cypher23 is a fictional story to raise awareness about privacy and information control` +
`\n\nMade with ❤️ by yeray@brokenlab` +
`\n\nCommands:` +
`\n\n- time-radio fut://cypher23@2668037535473: Play the space-time audio stream cypher23` +
`\n- time-radio --help: Display help information about the time-radio command` +
`\n- help: Display help information about the available commands`;
history.value.push({ command: input.value, response: helpResponse, when: Date.now() })
input.value = ''
}
</script>
<template>
<div>
<div v-for="command in history" :key="command.when">
<pre>$ {{ command.command }}</pre>
<pre>{{ command.response }}</pre>
</div>
</div>
<div class="prompt-wrapper" @click="() => autofocus()">
<div v-if="!promptLocked" class="prompt">$</div>
<pre class="user-input"> {{ input }}</pre>
<textarea ref="textarea" v-model="input" type="text" class="hidden-input" :disabled="promptLocked"></textarea>
<div id="cursor" class="cursor" v-if="!promptLocked"></div>
</div>
</template>
<style scoped>
.prompt-wrapper {
display: flex;
flex-direction: row;
align-items: center;
justify-content: start;
}
pre {
margin: 0;
padding: 0;
color: #0f0;
}
.prompt {
color: #0f0;
}
.cursor {
width: 1ch;
height: 1em;
background-color: #0f0;
animation: blink 1s infinite;
}
.hidden-input {
position: absolute;
left: -9999px;
}
@keyframes blink {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0;
}
}
</style>

1
src/env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="astro/client" />

72
src/pages/index.astro Normal file
View File

@ -0,0 +1,72 @@
---
import Prompt from "../components/Prompt.vue";
---
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.png" />
<meta name="viewport" content="width=device-width" />
<meta name="generator" content="" />
<title>Cypher23</title>
<style>
:root {
cursor: none;
--cursorX: 50vw;
--cursorY: 50vh;
}
:root:before {
content: '';
display: block;
width: 100%;
height: 100%;
position: fixed;
pointer-events: none;
background: radial-gradient(
circle 10vmax at var(--cursorX) var(--cursorY),
rgba(0,0,0,0) 0%,
rgba(0,0,0,.5) 80%,
rgba(0,0,0,.99) 100%
);
z-index: 9999;
}
</style>
</head>
<body>
<div class="terminal">
<Prompt client:only="vue"/>
</div>
<div class="bromessage">Totally legit government friendly authorized site, trust me bro</div>
<script>
function update(e: MouseEvent | TouchEvent){
const x = e instanceof MouseEvent ? e.clientX : e.touches[0].clientX
const y = e instanceof MouseEvent ? e.clientY : e.touches[0].clientY
document.documentElement.style.setProperty('--cursorX',x+'px')
document.documentElement.style.setProperty('--cursorY',y+'px')
}
document.addEventListener('mousemove',update)
document.addEventListener('touchmove',update)
</script>
<style>
body {
background-color: #222;
font-family: monospace;
color: #0f0;
}
.bromessage {
position:fixed;
top: 40%;
left: 25%;
z-index: 9999;
font-size: 4rem;
text-align: center;
width: 50%;
color: #eee;
font-family: serif;
}
</style>
</body>
</html>

6
tsconfig.json Normal file
View File

@ -0,0 +1,6 @@
{
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"jsx": "preserve"
}
}