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