├── index.json
├── .gitignore
├── .babelrc
├── .datignore
├── index.js
├── index.html
├── lib
├── sass-to-css
└── load-cassette.js
├── README.md
├── src
└── get-sass.js
├── components
├── cover-art.js
├── history.js
└── player.js
├── package.json
└── app.js
/index.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "boom-box"
3 | }
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | build
3 | style.css
4 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "@babel/preset-env"
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/.datignore:
--------------------------------------------------------------------------------
1 | .git
2 | .dat
3 | node_modules
4 | build
5 | *.log
6 | **/.DS_Store
7 | Thumbs.db
8 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | import Vue from './lib/vue.js'
2 | import App from './app.js'
3 |
4 | window.addEventListener('DOMContentLoaded', () => {
5 | new Vue(App) // eslint-disable-line
6 | })
7 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | boom box
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/lib/sass-to-css:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | const stdin = process.openStdin()
4 | const { renderSync } = require('node-sass')
5 |
6 | var sass = ''
7 |
8 | stdin.on('data', (chunk) => {
9 | sass += chunk
10 | })
11 |
12 | stdin.on('end', () => {
13 | const results = renderSync({ data: sass })
14 |
15 | console.log(results.css.toString())
16 | })
17 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # boom-box
2 |
3 | ```
4 | npm install
5 | npm run build // builds css from components
6 | ```
7 |
8 | start a BeakerBrowser project pointing at this folder
9 |
10 | Check it out in beaker, add the URI of a DAT folder with music in it that you're already seeding. ENJOY
11 |
12 | **NOTE** you need to re-run `npm run build` any time you change the css.
13 | You can run `npm run dev` which auto builds the css every 3 seconds. Crude but effective
14 |
15 |
--------------------------------------------------------------------------------
/src/get-sass.js:
--------------------------------------------------------------------------------
1 | import App from '../app.js'
2 |
3 | // this module has to be transpiled to be run by node (because of the es6 imports)
4 |
5 | const sass = getStyles(App)
6 |
7 | console.log(sass)
8 |
9 | function getStyles (node, results = '') {
10 | results += node.styles || ''
11 |
12 | if (node.components) {
13 | results += Object.values(node.components)
14 | .map(child => {
15 | return getStyles(child, results)
16 | })
17 | .join('\n')
18 | }
19 |
20 | return results
21 | }
22 |
--------------------------------------------------------------------------------
/components/cover-art.js:
--------------------------------------------------------------------------------
1 | const template = `
2 |
3 |
![]()
4 |
5 | `
6 |
7 | const styles = `
8 | .CoverArt {
9 | img {
10 | max-width: 100%
11 | }
12 | }
13 | `
14 |
15 | // TODO if multiple art, then could give option to scroll through them
16 |
17 | export default {
18 | props: {
19 | art: Array
20 | },
21 | computed: {
22 | src () {
23 | if (!this.art.length) return ''
24 | if (!this.art[0].uri) return ''
25 |
26 | return this.art[0].uri
27 | }
28 | },
29 | template,
30 | styles
31 | }
32 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "boom-box",
3 | "version": "0.0.1",
4 | "description": "play dats which have mp3s in them",
5 | "main": "index.js",
6 | "scripts": {
7 | "build": "run-p build:css",
8 | "build:css": "mkdir -p build && run-s build:get-sass build:run-sass",
9 | "build:get-sass": "browserify src/get-sass.js > build/get-sass.js ",
10 | "build:run-sass": "node build/get-sass.js | lib/sass-to-css > style.css",
11 | "dev": "while true; do npm run build:css && sleep 3; done"
12 | },
13 | "browserify": {
14 | "transform": [
15 | "babelify"
16 | ]
17 | },
18 | "devDependencies": {
19 | "@babel/core": "^7.4.5",
20 | "@babel/preset-env": "^7.4.5",
21 | "babelify": "^10.0.0",
22 | "browserify": "^16.2.3",
23 | "node-sass": "^4.12.0",
24 | "npm-run-all": "^4.1.5"
25 | },
26 | "repository": {
27 | "type": "git",
28 | "url": "git+https://github.com/mixmix/boom-box.git"
29 | },
30 | "keywords": [
31 | "DAT",
32 | "music",
33 | "player"
34 | ],
35 | "author": "mixmix",
36 | "license": "AGPL-3.0",
37 | "bugs": {
38 | "url": "https://github.com/mixmix/boom-box/issues"
39 | },
40 | "homepage": "https://github.com/mixmix/boom-box#readme"
41 | }
42 |
--------------------------------------------------------------------------------
/components/history.js:
--------------------------------------------------------------------------------
1 | const template = `
2 |
3 |
4 |
5 | {{tape.name}}
6 |
7 |
8 | `
9 |
10 | const styles = `
11 | .History {
12 | font-family: arial;
13 |
14 | > .title {
15 | font-size: .8rem;
16 | letter-spacing: 1px;
17 | border-bottom: 1px solid black;
18 | }
19 |
20 | > .tape {
21 | padding: 4px;
22 | cursor: pointer;
23 |
24 | &.selected {
25 | background: black;
26 | color: white;
27 | }
28 | }
29 | }
30 | `
31 |
32 | export default {
33 | props: {
34 | uri: String,
35 | name: String
36 | },
37 | data: () => ({
38 | history: []
39 | }),
40 | methods: {
41 | tapeClass (testTape) {
42 | return testTape.uri === this.uri ? 'selected' : ''
43 | }
44 | },
45 | created () {
46 | this.history = JSON.parse(localStorage.boomBox || '[]') // eslint-disable-line
47 | .sort((a, b) => (b.name > a.name) ? -1 : +1)
48 | },
49 | watch: {
50 | uri (next) {
51 | if (!next.length) return
52 |
53 | const newHistory = [ ...this.history ]
54 | .filter(tape => tape.uri !== next)
55 |
56 | newHistory.unshift({ uri: next, name: this.name })
57 | newHistory.sort((a, b) => (b.name > a.name) ? -1 : +1)
58 |
59 | this.history = newHistory
60 | },
61 | history (next) {
62 | localStorage.boomBox = JSON.stringify(this.history) // eslint-disable-line
63 | }
64 | },
65 | template,
66 | styles
67 | }
68 |
--------------------------------------------------------------------------------
/lib/load-cassette.js:
--------------------------------------------------------------------------------
1 | const musicExtensionRegexp = /\.(mp3|m4a|aac|ogg|wav)$/
2 | const numberPrefixRegexp = /^\d+\s*/
3 | const artExtensionRegexp = /\.(jpg|jpeg|png|gif)$/
4 |
5 | export default function loadCassette (link, cb) {
6 | if (!link.endsWith('/')) link = link + '/'
7 |
8 | const archive = beaker.hyperdrive.drive(link) // eslint-disable-line
9 |
10 | archive.readdir('/')
11 | .then(dir => {
12 | const music = dir
13 | .filter(isMusic)
14 | .sort()
15 | .map(filename => {
16 | return {
17 | uri: join(link, filename),
18 | name: trackName(filename)
19 | }
20 | })
21 |
22 | const art = dir
23 | .filter(isArt)
24 | .sort()
25 | .map(filename => {
26 | return {
27 | uri: join(link, filename)
28 | }
29 | })
30 |
31 | archive.readFile('index.json')
32 | .then(str => {
33 | const { title } = JSON.parse(str)
34 | cb(null, { name: title, music, art })
35 | })
36 | .catch(err => {
37 | console.error(err)
38 | cb(null, { name: '???', music, art })
39 | })
40 | })
41 | .catch(err => cb(err))
42 | }
43 |
44 | function isMusic (filename) {
45 | return filename.match(musicExtensionRegexp)
46 | }
47 |
48 | function isArt (filename) {
49 | return filename.match(artExtensionRegexp)
50 | }
51 |
52 | function join (link, track) {
53 | return link + track
54 | }
55 |
56 | function trackName (filename) {
57 | return filename
58 | .replace(numberPrefixRegexp, '')
59 | .replace(musicExtensionRegexp, '')
60 | }
61 |
--------------------------------------------------------------------------------
/components/player.js:
--------------------------------------------------------------------------------
1 | const template = `
2 |
3 |
4 |
5 |
6 |
{{i < 9 ? ('0' + (i+1)) : (i+1)}}
7 |
8 |
{{track.name}}
9 |
10 |
11 |
12 | `
13 |
14 | const styles = `
15 | .Player {
16 | > audio {
17 | width: 100%;
18 | filter: brightness(1.1);
19 | margin-bottom: 1rem;
20 | }
21 |
22 | > .tracks {
23 | > .track {
24 | cursor: pointer;
25 |
26 | display: grid;
27 | grid-template-columns: auto auto 1fr;
28 | grid-gap: 10px;
29 |
30 | &.selected {
31 | background: black;
32 | color: white;
33 | }
34 |
35 | > .num {
36 | font-size: .8rem;
37 | letter-spacing: 1px;
38 | padding: 3px 0 3px 13px;
39 | }
40 | > .divider { border-left: 1px solid black; }
41 | > .name {
42 | padding: 3px 0;
43 | }
44 | }
45 | }
46 | }
47 | `
48 |
49 | export default {
50 | props: {
51 | music: Array
52 | },
53 | data: () => ({
54 | currentTrack: null
55 | }),
56 | methods: {
57 | selectTrack (i) {
58 | this.currentTrack = i
59 | },
60 | nextTrack () {
61 | if (this.currentTrack === this.music.length - 1) this.currentTrack = null
62 | else this.currentTrack = this.currentTrack + 1
63 | },
64 | trackClass (i) {
65 | return i === this.currentTrack ? 'selected' : ''
66 | }
67 | },
68 | watch: {
69 | currentTrack (next, prev) {
70 | if (next === null) {
71 | this.$refs.player.pause()
72 | return
73 | }
74 |
75 | this.$refs.player.src = this.music[next].uri
76 | this.$refs.player.load()
77 | },
78 | music (next) {
79 | this.currentTrack = 0
80 | this.$refs.player.src = this.music[0].uri
81 | this.$refs.player.load()
82 | }
83 | },
84 | created () {
85 | this.currentTrack = 0
86 | },
87 | template,
88 | styles
89 | }
90 |
--------------------------------------------------------------------------------
/app.js:
--------------------------------------------------------------------------------
1 | import loadCassette from './lib/load-cassette.js'
2 | import Player from './components/player.js'
3 | import CoverArt from './components/cover-art.js'
4 | import History from './components/history.js'
5 |
6 | const template = `
7 |
8 |
{{name}}
9 |
10 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | `
21 |
22 | // example tape:
23 | // hyper://ef69934eb101628180d7dfa72ef04df038b534c943be510b4e4bb8f2d7b5e6b5
24 |
25 | const App = { // eslint-disable-line
26 | el: '#app',
27 | data: {
28 | uri: '',
29 | name: 'boom-box',
30 | music: [],
31 | art: []
32 | },
33 | computed: {
34 | isPlayable () {
35 | return this.music.length > 0
36 | }
37 | },
38 | methods: {
39 | processInput (ev) {
40 | var link = ev.target.value.trim()
41 | if (!isDriveURI(link)) return
42 |
43 | if (link === this.uri) return
44 | this.playCassette(link)
45 | },
46 | playCassette (link) {
47 | loadCassette(link, (err, data) => {
48 | if (err) {
49 | console.error(err)
50 | // TODO ... display to user
51 | return
52 | }
53 |
54 | this.uri = link
55 | this.$refs.input.value = link
56 | this.name = data.name
57 | this.music = data.music
58 | this.art = data.art
59 | })
60 | }
61 | },
62 | template,
63 | components: {
64 | Player,
65 | CoverArt,
66 | History
67 | }
68 | }
69 |
70 | App.styles = `
71 | .App {
72 | font-family: arial;
73 | padding: 2rem;
74 |
75 | >h1 {
76 | margin: 0;
77 | }
78 |
79 | >div {
80 | display: grid;
81 | grid-template-columns: 40rem 30rem;
82 | grid-gap: 4rem;
83 | justify-content: start;
84 |
85 | > .left {
86 |
87 | > .Player {
88 | margin-top: 1rem;
89 | }
90 | }
91 |
92 | > .right {
93 | max-width: 30rem;
94 |
95 | > input {
96 | width: 30rem;
97 | padding: 5px;
98 | margin-bottom: 3rem;
99 | }
100 | }
101 | }
102 | }
103 | `
104 |
105 | export default App
106 |
107 | function isDriveURI (link) {
108 | return link.startsWith('hyper://')
109 | }
110 |
--------------------------------------------------------------------------------