2023-09-22 05:48:54 +02:00
|
|
|
<template>
|
2023-10-22 10:44:32 +02:00
|
|
|
<div v-if="!available" class="mod-player-disabled">
|
2023-09-22 05:48:54 +02:00
|
|
|
<MkLoading v-if="fetching" />
|
|
|
|
<MkError v-else-if="error" @retry="load()" />
|
|
|
|
</div>
|
2023-10-22 10:44:32 +02:00
|
|
|
<div v-else-if="hide" class="mod-player-disabled" @click="toggleVisible()">
|
2023-09-22 05:48:54 +02:00
|
|
|
<div>
|
2023-10-17 03:57:20 +02:00
|
|
|
<b><i class="ph-warning"></i> {{ i18n.ts.sensitive }}</b>
|
2023-09-22 05:48:54 +02:00
|
|
|
<span>{{ i18n.ts.clickToShow }}</span>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
2023-10-22 10:44:32 +02:00
|
|
|
<div v-else class="mod-player-enabled">
|
2023-09-22 05:48:54 +02:00
|
|
|
<div class="pattern-display">
|
2023-10-22 10:44:32 +02:00
|
|
|
<div v-if="patternShow" ref="modPattern" class="mod-pattern">
|
2023-09-22 05:48:54 +02:00
|
|
|
<span
|
|
|
|
v-for="(row, i) in patData[currentPattern]"
|
|
|
|
v-if="patData.length !== 0"
|
2023-10-22 10:44:32 +02:00
|
|
|
ref="initRow"
|
|
|
|
:class="{ modRowActive: isRowActive(i) }"
|
2023-09-22 05:48:54 +02:00
|
|
|
>
|
2023-10-22 10:44:32 +02:00
|
|
|
<span :class="{ modColQuarter: i % 4 === 0 }">{{
|
2023-09-22 05:48:54 +02:00
|
|
|
indexText(i)
|
|
|
|
}}</span>
|
|
|
|
<span class="mod-row-inner">{{ getRowText(row) }}</span>
|
|
|
|
</span>
|
|
|
|
<MkLoading v-else />
|
|
|
|
</div>
|
2023-10-22 10:44:32 +02:00
|
|
|
<div v-else class="mod-pattern" @click="showPattern()">
|
|
|
|
<span ref="initRow" class="modRowActive">
|
2023-09-22 05:48:54 +02:00
|
|
|
<span class="modColQuarter">00</span>
|
|
|
|
<span class="mod-row-inner">|F-12Ev10XEF</span>
|
|
|
|
</span>
|
|
|
|
<br />
|
|
|
|
<p>{{ i18n.ts.clickToShowPatterns }}</p>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div class="controls">
|
2023-10-22 10:44:32 +02:00
|
|
|
<button v-if="!loading" class="play" @click="playPause()">
|
|
|
|
<i v-if="playing" class="ph-pause ph-fill"></i>
|
|
|
|
<i v-else class="ph-play ph-fill"></i>
|
2023-09-22 05:48:54 +02:00
|
|
|
</button>
|
|
|
|
<MkLoading v-else :em="true" />
|
|
|
|
<button class="stop" @click="stop()">
|
2023-10-17 03:57:20 +02:00
|
|
|
<i class="ph-stop ph-fill"></i>
|
2023-09-22 05:48:54 +02:00
|
|
|
</button>
|
|
|
|
<button class="loop" @click="toggleLoop()">
|
2023-10-22 10:44:32 +02:00
|
|
|
<i v-if="loop === -1" class="ph-repeat ph-fill"></i>
|
|
|
|
<i v-else class="ph-repeat-once ph-fill"></i>
|
2023-09-22 05:48:54 +02:00
|
|
|
</button>
|
|
|
|
<FormRange
|
2023-10-22 10:44:32 +02:00
|
|
|
ref="progress"
|
|
|
|
v-model="position"
|
2023-09-22 05:48:54 +02:00
|
|
|
class="progress"
|
|
|
|
:min="0"
|
|
|
|
:max="length"
|
|
|
|
:step="0.1"
|
|
|
|
:background="false"
|
|
|
|
:tooltips="false"
|
|
|
|
:instant="true"
|
|
|
|
@update:modelValue="performSeek()"
|
|
|
|
></FormRange>
|
|
|
|
<button class="mute" @click="toggleMute()">
|
2023-10-22 10:44:32 +02:00
|
|
|
<i v-if="muted" class="ph-speaker-simple-x ph-fill"></i>
|
|
|
|
<i v-else class="ph-speaker-simple-high ph-fill"></i>
|
2023-09-22 05:48:54 +02:00
|
|
|
</button>
|
|
|
|
<FormRange
|
2023-10-22 10:44:32 +02:00
|
|
|
v-model="player.context.gain.value"
|
2023-09-22 05:48:54 +02:00
|
|
|
class="volume"
|
|
|
|
:min="0"
|
|
|
|
:max="1"
|
|
|
|
:step="0.1"
|
|
|
|
:background="false"
|
|
|
|
:tooltips="false"
|
|
|
|
:instant="true"
|
|
|
|
@update:modelValue="updateMute()"
|
|
|
|
></FormRange>
|
|
|
|
<a
|
|
|
|
class="download"
|
|
|
|
:title="i18n.ts.download"
|
|
|
|
:href="module.url"
|
|
|
|
target="_blank"
|
|
|
|
>
|
2023-10-17 03:57:20 +02:00
|
|
|
<i class="ph-download-simple ph-fill"></i>
|
2023-09-22 05:48:54 +02:00
|
|
|
</a>
|
|
|
|
</div>
|
|
|
|
<div class="buttons">
|
|
|
|
<button
|
|
|
|
v-if="module.comment"
|
|
|
|
v-tooltip="i18n.ts.alt"
|
|
|
|
class="_button"
|
|
|
|
@click.stop="captionPopup"
|
|
|
|
>
|
2023-10-31 11:21:59 +01:00
|
|
|
<i :class="icon('ph-subtitles')"></i>
|
2023-09-22 05:48:54 +02:00
|
|
|
</button>
|
|
|
|
<button
|
|
|
|
v-if="!hide"
|
|
|
|
v-tooltip="i18n.ts.hide"
|
|
|
|
class="_button"
|
|
|
|
@click.stop="toggleVisible()"
|
|
|
|
>
|
2023-10-31 11:21:59 +01:00
|
|
|
<i :class="icon('ph-eye-slash')"></i>
|
2023-09-22 05:48:54 +02:00
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</template>
|
|
|
|
|
|
|
|
<script lang="ts" setup>
|
2023-10-22 10:44:32 +02:00
|
|
|
import { nextTick, onDeactivated, onMounted, ref, shallowRef } from "vue";
|
2024-02-12 16:40:46 +01:00
|
|
|
import type { entities } from "firefish-js";
|
2023-09-22 05:48:54 +02:00
|
|
|
import FormRange from "./form/range.vue";
|
|
|
|
import { i18n } from "@/i18n";
|
|
|
|
import * as os from "@/os";
|
|
|
|
import { defaultStore } from "@/store";
|
2023-10-22 10:44:32 +02:00
|
|
|
import { ChiptuneJsConfig, ChiptuneJsPlayer } from "@/scripts/chiptune2";
|
2023-10-17 03:57:20 +02:00
|
|
|
import icon from "@/scripts/icon";
|
2023-09-22 05:48:54 +02:00
|
|
|
|
|
|
|
const props = defineProps<{
|
2024-02-12 16:40:46 +01:00
|
|
|
module: entities.DriveFile;
|
2023-09-22 05:48:54 +02:00
|
|
|
}>();
|
|
|
|
|
|
|
|
interface ModRow {
|
|
|
|
notes: string[];
|
|
|
|
insts: string[];
|
|
|
|
vols: string[];
|
|
|
|
fxs: string[];
|
|
|
|
ops: string[];
|
|
|
|
}
|
|
|
|
|
|
|
|
const available = ref(false);
|
|
|
|
const initRow = shallowRef<HTMLSpanElement>();
|
|
|
|
const player = shallowRef(new ChiptuneJsPlayer(new ChiptuneJsConfig()));
|
2023-10-22 10:44:32 +02:00
|
|
|
const hide = ref(
|
2023-09-22 05:48:54 +02:00
|
|
|
defaultStore.state.nsfw === "force"
|
|
|
|
? true
|
|
|
|
: props.module.isSensitive && defaultStore.state.nsfw !== "ignore",
|
|
|
|
);
|
2023-10-22 10:44:32 +02:00
|
|
|
const playing = ref(false);
|
|
|
|
const patternShow = ref(false);
|
|
|
|
const modPattern = ref<HTMLDivElement>();
|
|
|
|
const progress = ref<typeof FormRange>();
|
|
|
|
const position = ref(0);
|
|
|
|
const patData = shallowRef([] as ModRow[][]);
|
|
|
|
const currentPattern = ref(0);
|
|
|
|
const nbChannels = ref(0);
|
|
|
|
const length = ref(1);
|
|
|
|
const muted = ref(false);
|
|
|
|
const loop = ref(0);
|
|
|
|
const fetching = ref(true);
|
|
|
|
const error = ref(false);
|
|
|
|
const loading = ref(false);
|
2023-09-22 05:48:54 +02:00
|
|
|
|
|
|
|
function load() {
|
|
|
|
player.value
|
|
|
|
.load(props.module.url)
|
|
|
|
.then((result: null) => {
|
|
|
|
buffer = result;
|
|
|
|
available.value = true;
|
|
|
|
error.value = false;
|
|
|
|
fetching.value = false;
|
|
|
|
})
|
|
|
|
.catch((e: any) => {
|
|
|
|
console.error(e);
|
|
|
|
error.value = true;
|
|
|
|
fetching.value = false;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
onMounted(load);
|
|
|
|
|
2023-10-22 10:44:32 +02:00
|
|
|
let currentRow = 0,
|
|
|
|
rowHeight = 0,
|
|
|
|
buffer = null,
|
|
|
|
isSeeking = false;
|
2023-09-22 05:48:54 +02:00
|
|
|
|
|
|
|
function captionPopup() {
|
|
|
|
os.alert({
|
|
|
|
type: "info",
|
|
|
|
text: props.module.comment,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function showPattern() {
|
|
|
|
patternShow.value = !patternShow.value;
|
|
|
|
nextTick(() => {
|
|
|
|
if (playing.value) display();
|
|
|
|
else stop();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function getRowText(row: ModRow) {
|
|
|
|
let text = "";
|
|
|
|
for (let i = 0; i < row.notes.length; i++) {
|
|
|
|
text = text.concat(
|
|
|
|
"|",
|
|
|
|
row.notes[i],
|
|
|
|
row.insts[i],
|
|
|
|
row.vols[i],
|
|
|
|
row.fxs[i],
|
|
|
|
row.ops[i],
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return text;
|
|
|
|
}
|
|
|
|
|
|
|
|
function playPause() {
|
|
|
|
player.value.addHandler("onRowChange", (i: { index: number }) => {
|
|
|
|
currentRow = i.index;
|
|
|
|
currentPattern.value = player.value.getPattern();
|
|
|
|
length.value = player.value.duration();
|
|
|
|
if (!isSeeking) {
|
|
|
|
position.value = player.value.position();
|
|
|
|
}
|
|
|
|
requestAnimationFrame(display);
|
|
|
|
});
|
|
|
|
|
|
|
|
player.value.addHandler("onEnded", () => {
|
|
|
|
stop();
|
|
|
|
});
|
|
|
|
|
|
|
|
if (player.value.currentPlayingNode === null) {
|
|
|
|
loading.value = true;
|
|
|
|
player.value.play(buffer).then(() => {
|
|
|
|
player.value.seek(position.value);
|
|
|
|
player.value.repeat(loop.value);
|
|
|
|
playing.value = true;
|
|
|
|
loading.value = false;
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
player.value.togglePause();
|
|
|
|
playing.value = !player.value.currentPlayingNode.paused;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async function stop(noDisplayUpdate = false) {
|
|
|
|
player.value.stop();
|
|
|
|
playing.value = false;
|
|
|
|
if (!noDisplayUpdate) {
|
|
|
|
try {
|
|
|
|
await player.value.play(buffer);
|
|
|
|
display(0, true);
|
|
|
|
} catch (e) {
|
|
|
|
console.warn(e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
player.value.stop();
|
|
|
|
position.value = 0;
|
|
|
|
currentRow = 0;
|
|
|
|
player.value.clearHandlers();
|
|
|
|
}
|
|
|
|
|
|
|
|
function toggleLoop() {
|
|
|
|
loop.value = loop.value === -1 ? 0 : -1;
|
|
|
|
player.value.repeat(loop.value);
|
|
|
|
}
|
|
|
|
|
|
|
|
let savedVolume = 0;
|
|
|
|
|
|
|
|
function toggleMute() {
|
|
|
|
if (muted.value) {
|
|
|
|
player.value.context.gain.value = savedVolume;
|
|
|
|
savedVolume = 0;
|
|
|
|
} else {
|
|
|
|
savedVolume = player.value.context.gain.value;
|
|
|
|
player.value.context.gain.value = 0;
|
|
|
|
}
|
|
|
|
muted.value = !muted.value;
|
|
|
|
}
|
|
|
|
|
|
|
|
function updateMute() {
|
|
|
|
muted.value = false;
|
|
|
|
savedVolume = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
function performSeek() {
|
|
|
|
player.value.seek(position.value);
|
|
|
|
display();
|
|
|
|
}
|
|
|
|
|
|
|
|
function toggleVisible() {
|
|
|
|
hide.value = !hide.value;
|
|
|
|
nextTick(() => {
|
|
|
|
stop(hide.value);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function isRowActive(i: number) {
|
|
|
|
if (i === currentRow) {
|
|
|
|
if (modPattern.value) {
|
|
|
|
if (rowHeight === 0 && initRow.value)
|
|
|
|
rowHeight = initRow.value[0].getBoundingClientRect().height;
|
|
|
|
modPattern.value.scrollTop = currentRow * rowHeight;
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function indexText(i: number) {
|
|
|
|
let rowText = i.toString(16);
|
|
|
|
if (rowText.length === 1) {
|
|
|
|
rowText = "0" + rowText;
|
|
|
|
}
|
|
|
|
return rowText;
|
|
|
|
}
|
|
|
|
|
|
|
|
function getRow(pattern: number, rowOffset: number) {
|
2023-10-22 10:44:32 +02:00
|
|
|
const notes: string[] = [];
|
|
|
|
const insts: string[] = [];
|
|
|
|
const vols: string[] = [];
|
|
|
|
const fxs: string[] = [];
|
|
|
|
const ops: string[] = [];
|
2023-09-22 05:48:54 +02:00
|
|
|
|
|
|
|
for (let channel = 0; channel < nbChannels.value; channel++) {
|
|
|
|
const part = player.value.getPatternRowChannel(
|
|
|
|
pattern,
|
|
|
|
rowOffset,
|
|
|
|
channel,
|
|
|
|
);
|
|
|
|
|
|
|
|
notes.push(part.substring(0, 3));
|
|
|
|
insts.push(part.substring(4, 6));
|
|
|
|
vols.push(part.substring(6, 9));
|
|
|
|
fxs.push(part.substring(10, 11));
|
|
|
|
ops.push(part.substring(11, 13));
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
notes,
|
|
|
|
insts,
|
|
|
|
vols,
|
|
|
|
fxs,
|
|
|
|
ops,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
function display(_time = 0, reset = false) {
|
|
|
|
if (!patternShow.value) return;
|
|
|
|
|
|
|
|
if (reset) {
|
|
|
|
const pattern = player.value.getPattern();
|
|
|
|
currentPattern.value = pattern;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (patData.value.length === 0) {
|
|
|
|
const nbPatterns = player.value.getNumPatterns();
|
|
|
|
const pattern = player.value.getPattern();
|
|
|
|
|
|
|
|
currentPattern.value = pattern;
|
|
|
|
|
|
|
|
if (player.value.currentPlayingNode) {
|
|
|
|
nbChannels.value = player.value.currentPlayingNode.nbChannels;
|
|
|
|
}
|
|
|
|
|
|
|
|
const patternsArray: ModRow[][] = [];
|
|
|
|
|
|
|
|
for (let patOffset = 0; patOffset < nbPatterns; patOffset++) {
|
|
|
|
const rowsArray: ModRow[] = [];
|
|
|
|
const nbRows = player.value.getPatternNumRows(patOffset);
|
|
|
|
for (let rowOffset = 0; rowOffset < nbRows; rowOffset++) {
|
|
|
|
rowsArray.push(getRow(patOffset, rowOffset));
|
|
|
|
}
|
|
|
|
patternsArray.push(rowsArray);
|
|
|
|
}
|
|
|
|
|
|
|
|
patData.value = Object.freeze(patternsArray);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
onDeactivated(() => {
|
|
|
|
stop();
|
|
|
|
});
|
|
|
|
</script>
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
|
.mod-player-enabled {
|
|
|
|
position: relative;
|
|
|
|
display: flex;
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
|
|
> i {
|
|
|
|
display: block;
|
|
|
|
position: absolute;
|
|
|
|
border-radius: 6px;
|
|
|
|
background-color: var(--fg);
|
|
|
|
color: var(--accentLighten);
|
|
|
|
font-size: 14px;
|
|
|
|
opacity: 0.5;
|
|
|
|
padding: 3px 6px;
|
|
|
|
text-align: center;
|
|
|
|
cursor: pointer;
|
|
|
|
top: 12px;
|
|
|
|
right: 12px;
|
|
|
|
}
|
|
|
|
|
|
|
|
> .buttons {
|
|
|
|
display: flex;
|
|
|
|
gap: 4px;
|
|
|
|
position: absolute;
|
|
|
|
border-radius: 6px;
|
|
|
|
overflow: hidden;
|
|
|
|
top: 12px;
|
|
|
|
right: 12px;
|
|
|
|
> * {
|
|
|
|
background-color: var(--accentedBg);
|
|
|
|
-webkit-backdrop-filter: var(--blur, blur(15px));
|
|
|
|
backdrop-filter: var(--blur, blur(15px));
|
|
|
|
color: var(--accent);
|
|
|
|
font-size: 0.8em;
|
|
|
|
padding: 6px 8px;
|
|
|
|
text-align: center;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
> .pattern-display {
|
|
|
|
width: 100%;
|
|
|
|
height: 100%;
|
|
|
|
overflow: hidden;
|
2023-09-22 06:55:57 +02:00
|
|
|
color: var(--fg);
|
|
|
|
background-color: var(--panelHighlight);
|
2023-09-22 05:48:54 +02:00
|
|
|
text-align: center;
|
|
|
|
font: 12px monospace;
|
|
|
|
white-space: pre;
|
|
|
|
user-select: none;
|
|
|
|
|
|
|
|
> .mod-pattern {
|
|
|
|
display: grid;
|
|
|
|
overflow-y: hidden;
|
|
|
|
height: 0;
|
|
|
|
padding-top: calc((56.25% - 48px) / 2);
|
|
|
|
padding-bottom: calc((56.25% - 48px) / 2);
|
|
|
|
content-visibility: auto;
|
|
|
|
|
|
|
|
> .modRowActive {
|
|
|
|
opacity: 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
> span {
|
|
|
|
opacity: 0.5;
|
|
|
|
|
|
|
|
> .modColQuarter {
|
2023-09-22 06:55:57 +02:00
|
|
|
color: var(--badge);
|
2023-09-22 05:48:54 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
> .mod-row-inner {
|
|
|
|
background: repeating-linear-gradient(
|
|
|
|
to right,
|
2023-09-22 06:55:57 +02:00
|
|
|
var(--fg) 0 4ch,
|
|
|
|
var(--codeBoolean) 4ch 6ch,
|
2023-09-22 07:19:44 +02:00
|
|
|
var(--codeNumber) 6ch 9ch,
|
2023-09-22 06:55:57 +02:00
|
|
|
var(--codeString) 9ch 10ch,
|
|
|
|
var(--error) 10ch 12ch
|
2023-09-22 05:48:54 +02:00
|
|
|
);
|
|
|
|
background-clip: text;
|
|
|
|
-webkit-background-clip: text;
|
|
|
|
-webkit-text-fill-color: transparent;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
> .controls {
|
|
|
|
display: flex;
|
|
|
|
width: 100%;
|
|
|
|
background-color: var(--panelHighlight);
|
|
|
|
|
|
|
|
> * {
|
|
|
|
padding: 4px 8px;
|
|
|
|
}
|
|
|
|
|
|
|
|
> button,
|
|
|
|
a {
|
|
|
|
border: none;
|
|
|
|
background-color: transparent;
|
|
|
|
color: var(--navFg);
|
|
|
|
cursor: pointer;
|
|
|
|
margin: auto;
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
background-color: var(--accentedBg);
|
|
|
|
border-radius: 3px;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
> .progress {
|
|
|
|
flex-grow: 1;
|
|
|
|
min-width: 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
> .volume {
|
|
|
|
flex-shrink: 1;
|
|
|
|
max-width: 128px;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.mod-player-disabled {
|
|
|
|
display: flex;
|
|
|
|
justify-content: center;
|
|
|
|
align-items: center;
|
2023-09-22 06:55:57 +02:00
|
|
|
background: var(--infoWarnBg);
|
|
|
|
color: var(--infoWarnFg);
|
2023-09-22 05:48:54 +02:00
|
|
|
|
|
|
|
> div {
|
|
|
|
display: table-cell;
|
|
|
|
text-align: center;
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
|
|
> b {
|
|
|
|
display: block;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
</style>
|