base commit
This commit is contained in:
commit
9c5fb97cdc
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal 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
4
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["astro-build.astro-vscode"],
|
||||||
|
"unwantedRecommendations": []
|
||||||
|
}
|
11
.vscode/launch.json
vendored
Normal file
11
.vscode/launch.json
vendored
Normal 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
5
README.md
Normal 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
8
astro.config.mjs
Normal 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
7599
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
19
package.json
Normal file
19
package.json
Normal 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
BIN
public/cypher23.mp3
Normal file
Binary file not shown.
BIN
public/favicon.png
Normal file
BIN
public/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 37 KiB |
244
src/components/Prompt.vue
Normal file
244
src/components/Prompt.vue
Normal 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
1
src/env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/// <reference types="astro/client" />
|
72
src/pages/index.astro
Normal file
72
src/pages/index.astro
Normal 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
6
tsconfig.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"extends": "astro/tsconfigs/strict",
|
||||||
|
"compilerOptions": {
|
||||||
|
"jsx": "preserve"
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user