Merge branch 'develop' into feat/scylladb

This commit is contained in:
Namekuji 2023-09-22 10:10:57 -04:00
commit f0d3f71656
No known key found for this signature in database
GPG key ID: 1D62332C07FBA532
25 changed files with 2939 additions and 1483 deletions

View file

@ -28,6 +28,10 @@ Machine learning model for sensitive images by Infinite Red, Inc.
License: MIT
https://github.com/infinitered/nsfwjs/blob/master/LICENSE
Chiptune2.js by Simon Gündling
License: MIT
https://github.com/deskjet/chiptune2.js#license
Licenses for all softwares and software libraries installed via the Node Package Manager ("npm") can be found by running the following shell command in the root directory of this repository:
pnpm licenses list

View file

@ -4,6 +4,8 @@ Breaking changes are indecated by the :warning: icon.
## v1.0.5 (unreleased)
- Added `lang` parameter to `notes/create` and `notes/edit`.
### dev11
- :warning: `notes/translate` now requires credentials.

View file

@ -132,6 +132,7 @@ rememberNoteVisibility: "Remember post visibility settings"
attachCancel: "Remove attachment"
markAsSensitive: "Mark as NSFW"
unmarkAsSensitive: "Unmark as NSFW"
clickToShowPatterns: "Click to show module patterns"
enterFileName: "Enter filename"
mute: "Mute"
unmute: "Unmute"

View file

@ -120,6 +120,7 @@ rememberNoteVisibility: "Lembrar das configurações de visibilidade de notas"
attachCancel: "Remover anexo"
markAsSensitive: "Marcar como sensível"
unmarkAsSensitive: "Desmarcar como sensível"
clickToShowPatterns: "Clique para mostrar os padrões do módulo"
enterFileName: "Digite o nome do ficheiro"
mute: "Silenciar"
unmute: "Dessilenciar"

View file

@ -1,6 +1,6 @@
{
"name": "firefish",
"version": "1.0.5-dev16",
"version": "1.0.5-dev17",
"codename": "aqua",
"repository": {
"type": "git",

View file

@ -0,0 +1,13 @@
export class AddPostLang1695334243217 {
name = "AddPostLang1695334243217";
async up(queryRunner) {
await queryRunner.query(
`ALTER TABLE "note" ADD "lang" character varying(10)`,
);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "lang"`);
}
}

View file

@ -68,6 +68,15 @@ export const FILE_TYPE_BROWSERSAFE = [
"audio/x-flac",
"audio/flac",
"audio/vnd.wave",
"audio/mod",
"audio/x-mod",
"audio/s3m",
"audio/x-s3m",
"audio/xm",
"audio/x-xm",
"audio/it",
"audio/x-it",
];
/*
https://github.com/sindresorhus/file-type/blob/main/supported.js

View file

@ -66,6 +66,12 @@ export class Note {
})
public text: string | null;
@Column("varchar", {
length: 10,
nullable: true,
})
public lang: string | null;
@Column("varchar", {
length: 256,
nullable: true,

View file

@ -300,8 +300,6 @@ export const NoteRepository = db.getRepository(Note).extend({
host,
);
const lang =
detectLanguage(`${note.cw ?? ""}\n${note.text ?? ""}`) ?? "unknown";
const reactionEmoji = await populateEmojis(reactionEmojiNames, host);
const packed: Packed<"Note"> = await awaitAll({
id: note.id,
@ -376,7 +374,7 @@ export const NoteRepository = db.getRepository(Note).extend({
: undefined,
}
: {}),
lang: lang,
lang: note.lang,
});
if (packed.user.isCat && packed.user.speakAsCat && packed.text) {

View file

@ -64,6 +64,7 @@ import {
parseHomeTimeline,
} from "@/db/scylla.js";
import type { Client } from "cassandra-driver";
import { langmap } from "@/misc/langmap.js";
const logger = apLogger;
@ -316,11 +317,24 @@ export async function createNote(
// Text parsing
let text: string | null = null;
let lang: string | null = null;
if (
note.source?.mediaType === "text/x.misskeymarkdown" &&
typeof note.source?.content === "string"
) {
text = note.source.content;
if (note.contentMap != null) {
const key = Object.keys(note.contentMap)[0];
lang = Object.keys(langmap).includes(key)
? key.trim().split("-")[0].split("@")[0]
: null;
}
} else if (note.contentMap != null) {
const entry = Object.entries(note.contentMap)[0];
lang = Object.keys(langmap).includes(entry[0])
? entry[0].trim().split("-")[0].split("@")[0]
: null;
text = htmlToMfm(entry[1], note.tag);
} else if (typeof note.content === "string") {
text = htmlToMfm(note.content, note.tag);
}
@ -417,6 +431,7 @@ export async function createNote(
name: note.name,
cw,
text,
lang,
localOnly: false,
visibility,
visibleUsers,
@ -617,11 +632,24 @@ export async function updateNote(value: string | IObject, resolver?: Resolver) {
// Text parsing
let text: string | null = null;
let lang: string | null = null;
if (
post.source?.mediaType === "text/x.misskeymarkdown" &&
typeof post.source?.content === "string"
) {
text = post.source.content;
if (post.contentMap != null) {
const key = Object.keys(post.contentMap)[0];
lang = Object.keys(langmap).includes(key)
? key.trim().split("-")[0].split("@")[0]
: null;
}
} else if (post.contentMap != null) {
const entry = Object.entries(post.contentMap)[0];
lang = Object.keys(langmap).includes(entry[0])
? entry[0].trim().split("-")[0].split("@")[0]
: null;
text = htmlToMfm(entry[1], post.tag);
} else if (typeof post.content === "string") {
text = htmlToMfm(post.content, post.tag);
}
@ -717,6 +745,9 @@ export async function updateNote(value: string | IObject, resolver?: Resolver) {
if (text && text !== note.text) {
update.text = text;
}
if (lang && lang !== note.lang) {
update.lang = lang;
}
if (cw !== note.cw) {
update.cw = cw ? cw : null;
}

View file

@ -158,10 +158,12 @@ export default async function renderNote(
}),
);
const lang = detectLanguage(text);
const contentMap = lang ? {
[lang]: content
} : null;
const lang = note.lang ?? detectLanguage(text);
const contentMap = lang
? {
[lang]: content,
}
: null;
const emojis = await getEmojis(note.emojis);
const apemojis = emojis.map((emoji) => renderEmoji(emoji));

View file

@ -109,6 +109,7 @@ export const paramDef = {
},
},
text: { type: "string", maxLength: MAX_NOTE_TEXT_LENGTH, nullable: true },
lang: { type: "string", nullable: true, maxLength: 10 },
cw: { type: "string", nullable: true, maxLength: 100 },
localOnly: { type: "boolean", default: false },
noExtractMentions: { type: "boolean", default: false },
@ -305,6 +306,7 @@ export default define(meta, paramDef, async (ps, user) => {
}
: undefined,
text: ps.text || undefined,
lang: ps.lang,
reply,
renote,
cw: ps.cw,

View file

@ -1,5 +1,5 @@
import { In } from "typeorm";
import create, { index } from "@/services/note/create.js";
import { index } from "@/services/note/create.js";
import type { IRemoteUser, User } from "@/models/entities/user.js";
import {
Users,
@ -46,6 +46,8 @@ import {
parseHomeTimeline,
} from "@/db/scylla.js";
import type { Client } from "cassandra-driver";
import { detect as detectLanguage } from "tinyld";
import { langmap } from "@/misc/langmap.js";
export const meta = {
tags: ["notes"],
@ -180,6 +182,7 @@ export const paramDef = {
},
},
text: { type: "string", maxLength: MAX_NOTE_TEXT_LENGTH, nullable: true },
lang: { type: "string", nullable: true, maxLength: 10 },
cw: { type: "string", nullable: true, maxLength: 250 },
localOnly: { type: "boolean", default: false },
noExtractMentions: { type: "boolean", default: false },
@ -398,6 +401,16 @@ export default define(meta, paramDef, async (ps, user) => {
ps.text = null;
}
if (ps.lang) {
if (!Object.keys(langmap).includes(ps.lang.trim()))
throw new Error("invalid param");
ps.lang = ps.lang.trim().split("-")[0].split("@")[0];
} else if (ps.text) {
ps.lang = detectLanguage(ps.text);
} else {
ps.lang = null;
}
let tags = [];
let emojis = [];
let mentionedUsers = [];
@ -585,6 +598,9 @@ export default define(meta, paramDef, async (ps, user) => {
if (ps.text !== note.text) {
update.text = ps.text;
}
if (ps.lang !== note.lang) {
update.lang = ps.lang;
}
if (ps.cw !== note.cw || (ps.cw && !note.cw)) {
update.cw = ps.cw;
}

View file

@ -512,7 +512,7 @@ router.get("/notes/:note", async (ctx, next) => {
ctx.set("Cache-Control", "public, max-age=15");
ctx.set(
"Content-Security-Policy",
"default-src 'self' 'unsafe-inline'; img-src *; frame-ancestors *",
"default-src 'self' 'unsafe-inline' 'unsafe-eval'; connect-src *; font-src 'self' data:; img-src *; media-src *; worker-src 'self'; frame-ancestors *",
);
return;

View file

@ -81,6 +81,8 @@ import {
ScyllaPoll,
} from "@/db/scylla.js";
import { userByIdCache, userDenormalizedCache } from "../user-cache.js";
import { detect as detectLanguage } from "tinyld";
import { langmap } from "@/misc/langmap.js";
export const mutedWordsCache = new Cache<
{ userId: UserProfile["userId"]; mutedWords: UserProfile["mutedWords"] }[]
@ -153,6 +155,7 @@ type Option = {
createdAt?: Date | null;
name?: string | null;
text?: string | null;
lang?: string | null;
reply?: Note | null;
renote?: Note | null;
files?: DriveFile[] | null;
@ -290,6 +293,16 @@ export default async (
data.text = null;
}
if (data.lang) {
if (!Object.keys(langmap).includes(data.lang.trim()))
throw new Error("invalid param");
data.lang = data.lang.trim().split("-")[0].split("@")[0];
} else if (data.text) {
data.lang = detectLanguage(data.text);
} else {
data.lang = null;
}
let tags = data.apHashtags;
let emojis = data.apEmojis;
let mentionedUsers = data.apMentions;
@ -772,6 +785,7 @@ async function insertNote(
: null,
name: data.name,
text: data.text,
lang: data.lang,
hasPoll: data.poll != null,
cw: data.cw == null ? null : data.cw,
tags: tags.map((tag) => normalizeForSearch(tag)),

View file

@ -61,6 +61,7 @@
"insert-text-at-cursor": "0.3.0",
"json5": "2.2.3",
"katex": "0.16.8",
"libopenmpt-wasm": "github:TheEssem/libopenmpt-packaging#build",
"matter-js": "0.19.0",
"mfm-js": "0.23.3",
"photoswipe": "5.3.9",

View file

@ -4,7 +4,6 @@
v-tooltip="capitalize(instance.softwareName)"
class="hpaizdrt"
:style="bg"
@click.stop="openServerInfo"
>
<img class="icon" :src="getInstanceIcon(instance)" aria-hidden="true" />
<span class="name">{{ instance.name }}</span>
@ -17,8 +16,6 @@ import { ref } from "vue";
import { instanceName } from "@/config";
import { instance as Instance } from "@/instance";
import { getProxiedImageUrlNullable } from "@/scripts/media-proxy";
import { defaultStore } from "@/store";
import { pageWindow } from "@/os";
const props = defineProps<{
instance?: {
@ -27,7 +24,6 @@ const props = defineProps<{
themeColor?: string;
softwareName?: string;
};
host: string | null;
}>();
const ticker = ref<HTMLElement | null>(null);
@ -87,13 +83,6 @@ function getInstanceIcon(instance): string {
"/client-assets/dummy.png"
);
}
function openServerInfo() {
if (!defaultStore.state.openServerInfo) return;
const instanceInfoUrl =
props.host == null ? "/about" : `/instance-info/${props.host}`;
pageWindow(instanceInfoUrl);
}
</script>
<style lang="scss" scoped>

View file

@ -12,16 +12,28 @@
:class="{ dmWidth: inDm }"
>
<div ref="gallery" @click.stop>
<XMedia
<template
v-for="media in mediaList.filter((media) =>
previewable(media),
)"
:key="media.id"
:class="{ image: media.type.startsWith('image') }"
:data-id="media.id"
:media="media"
:raw="raw"
/>
>
<XMedia
v-if="
media.type.startsWith('video') ||
media.type.startsWith('image')
"
:key="media.id"
:class="{ image: media.type.startsWith('image') }"
:data-id="media.id"
:media="media"
:raw="raw"
/>
<XModPlayer
v-else-if="isModule(media)"
:key="media.id"
:module="media"
/>
</template>
</div>
</div>
</div>
@ -35,8 +47,13 @@ import PhotoSwipe from "photoswipe";
import "photoswipe/style.css";
import XBanner from "@/components/MkMediaBanner.vue";
import XMedia from "@/components/MkMedia.vue";
import XModPlayer from "@/components/MkModPlayer.vue";
import * as os from "@/os";
import { FILE_TYPE_BROWSERSAFE } from "@/const";
import {
FILE_TYPE_BROWSERSAFE,
FILE_TYPE_TRACKER_MODULES,
FILE_EXT_TRACKER_MODULES,
} from "@/const";
const props = defineProps<{
mediaList: misskey.entities.DriveFile[];
@ -170,11 +187,24 @@ onMounted(() => {
const previewable = (file: misskey.entities.DriveFile): boolean => {
if (file.type === "image/svg+xml") return true; // svgwebpublic/thumbnailpngtrue
// FILE_TYPE_BROWSERSAFE
if (isModule(file)) return true;
return (
(file.type.startsWith("video") || file.type.startsWith("image")) &&
FILE_TYPE_BROWSERSAFE.includes(file.type)
);
};
const isModule = (file: misskey.entities.DriveFile): boolean => {
return (
FILE_TYPE_TRACKER_MODULES.some((type) => {
return file.type === type;
}) ||
FILE_EXT_TRACKER_MODULES.some((ext) => {
return file.name.toLowerCase().endsWith("." + ext);
})
);
};
const previewableCount = props.mediaList.filter((media) =>
previewable(media),
).length;

View file

@ -0,0 +1,516 @@
<template>
<div class="mod-player-disabled" v-if="!available">
<MkLoading v-if="fetching" />
<MkError v-else-if="error" @retry="load()" />
</div>
<div class="mod-player-disabled" v-else-if="hide" @click="toggleVisible()">
<div>
<b
><i class="ph-warning ph-bold ph-lg"></i>
{{ i18n.ts.sensitive }}</b
>
<span>{{ i18n.ts.clickToShow }}</span>
</div>
</div>
<div class="mod-player-enabled" v-else>
<div class="pattern-display">
<div class="mod-pattern" ref="modPattern" v-if="patternShow">
<span
v-for="(row, i) in patData[currentPattern]"
ref="initRow"
v-bind:class="{ modRowActive: isRowActive(i) }"
v-if="patData.length !== 0"
>
<span v-bind:class="{ modColQuarter: i % 4 === 0 }">{{
indexText(i)
}}</span>
<span class="mod-row-inner">{{ getRowText(row) }}</span>
</span>
<MkLoading v-else />
</div>
<div class="mod-pattern" v-else @click="showPattern()">
<span class="modRowActive" ref="initRow">
<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">
<button class="play" @click="playPause()" v-if="!loading">
<i class="ph-pause ph-fill ph-lg" v-if="playing"></i>
<i class="ph-play ph-fill ph-lg" v-else></i>
</button>
<MkLoading v-else :em="true" />
<button class="stop" @click="stop()">
<i class="ph-stop ph-fill ph-lg"></i>
</button>
<button class="loop" @click="toggleLoop()">
<i class="ph-repeat ph-fill ph-lg" v-if="loop === -1"></i>
<i class="ph-repeat-once ph-fill ph-lg" v-else></i>
</button>
<FormRange
class="progress"
:min="0"
:max="length"
v-model="position"
:step="0.1"
ref="progress"
:background="false"
:tooltips="false"
:instant="true"
@update:modelValue="performSeek()"
></FormRange>
<button class="mute" @click="toggleMute()">
<i class="ph-speaker-simple-x ph-fill ph-lg" v-if="muted"></i>
<i class="ph-speaker-simple-high ph-fill ph-lg" v-else></i>
</button>
<FormRange
class="volume"
:min="0"
:max="1"
v-model="player.context.gain.value"
: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"
>
<i class="ph-download-simple ph-fill ph-lg"></i>
</a>
</div>
<div class="buttons">
<button
v-if="module.comment"
v-tooltip="i18n.ts.alt"
class="_button"
@click.stop="captionPopup"
>
<i class="ph-subtitles ph-bold ph-lg"></i>
</button>
<button
v-if="!hide"
v-tooltip="i18n.ts.hide"
class="_button"
@click.stop="toggleVisible()"
>
<i class="ph-eye-slash ph-bold ph-lg"></i>
</button>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, shallowRef, nextTick, onDeactivated, onMounted } from "vue";
import * as firefish from "firefish-js";
import FormRange from "./form/range.vue";
import { i18n } from "@/i18n";
import * as os from "@/os";
import { defaultStore } from "@/store";
import { ChiptuneJsPlayer, ChiptuneJsConfig } from "@/scripts/chiptune2";
const props = defineProps<{
module: firefish.entities.DriveFile;
}>();
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()));
let hide = ref(
defaultStore.state.nsfw === "force"
? true
: props.module.isSensitive && defaultStore.state.nsfw !== "ignore",
);
let playing = ref(false);
let patternShow = ref(false);
let modPattern = ref<HTMLDivElement>();
let progress = ref<typeof FormRange>();
let position = ref(0);
let patData = shallowRef([] as ModRow[][]);
let currentPattern = ref(0);
let nbChannels = ref(0);
let length = ref(1);
let muted = ref(false);
let loop = ref(0);
let fetching = ref(true);
let error = ref(false);
let loading = ref(false);
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);
let currentRow = 0;
let rowHeight = 0;
let buffer = null;
let isSeeking = false;
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;
}
return;
}
function indexText(i: number) {
let rowText = i.toString(16);
if (rowText.length === 1) {
rowText = "0" + rowText;
}
return rowText;
}
function getRow(pattern: number, rowOffset: number) {
let notes: string[] = [],
insts: string[] = [],
vols: string[] = [],
fxs: string[] = [],
ops: string[] = [];
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;
color: var(--fg);
background-color: var(--panelHighlight);
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 {
color: var(--badge);
}
> .mod-row-inner {
background: repeating-linear-gradient(
to right,
var(--fg) 0 4ch,
var(--codeBoolean) 4ch 6ch,
var(--codeNumber) 6ch 9ch,
var(--codeString) 9ch 10ch,
var(--error) 10ch 12ch
);
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;
background: var(--infoWarnBg);
color: var(--infoWarnFg);
> div {
display: table-cell;
text-align: center;
font-size: 12px;
> b {
display: block;
}
}
}
</style>

View file

@ -97,7 +97,11 @@
<div class="main">
<div class="header-container">
<MkAvatar class="avatar" :user="appearNote.user" />
<XNoteHeader class="header" :note="appearNote" />
<XNoteHeader
class="header"
:note="appearNote"
:can-open-server-info="true"
/>
</div>
<div class="body">
<MkSubNoteContent

View file

@ -40,7 +40,7 @@
v-if="showTicker"
class="ticker"
:instance="note.user.instance"
:host="note.user.host"
@click.stop="openServerInfo"
/>
</div>
</div>
@ -57,10 +57,12 @@ import MkInstanceTicker from "@/components/MkInstanceTicker.vue";
import { notePage } from "@/filters/note";
import { userPage } from "@/filters/user";
import { i18n } from "@/i18n";
import { pageWindow } from "@/os";
const props = defineProps<{
note: misskey.entities.Note;
pinned?: boolean;
canOpenServerInfo?: boolean;
}>();
const note = ref(props.note);
@ -69,6 +71,15 @@ const showTicker =
defaultStore.state.instanceTicker === "always" ||
(defaultStore.state.instanceTicker === "remote" &&
note.value.user.instance);
function openServerInfo() {
if (!props.canOpenServerInfo || !defaultStore.state.openServerInfo) return;
const instanceInfoUrl =
note.value.user.host == null
? "/about"
: `/instance-info/${note.value.user.host}`;
pageWindow(instanceInfoUrl);
}
</script>
<style lang="scss" scoped>

View file

@ -1,7 +1,7 @@
<template>
<label class="timctyfi" :class="{ disabled, easing }">
<div class="label"><slot name="label"></slot></div>
<div v-adaptive-border class="body">
<div v-adaptive-border class="body" :class="{ background }">
<div class="container">
<input
ref="inputEl"
@ -19,7 +19,12 @@
@touchend="tooltipHide"
@mouseenter="tooltipShow"
@mouseleave="tooltipHide"
@input="(x) => (inputVal = x.target.value)"
@input="
(x) => {
inputVal = x.target.value;
if (instant) onChange(x);
}
"
/>
<datalist v-if="showTicks && steps" :id="id">
<option
@ -50,11 +55,17 @@ const props = withDefaults(
textConverter?: (value: number) => string;
showTicks?: boolean;
easing?: boolean;
background?: boolean;
tooltips?: boolean;
instant?: boolean;
}>(),
{
step: 1,
textConverter: (v) => v.toString(),
easing: false,
background: true,
tooltips: true,
instant: false,
},
);
@ -79,6 +90,7 @@ function onChange(x) {
const tooltipShowing = ref(false);
function tooltipShow() {
if (!props.tooltips) return;
tooltipShowing.value = true;
os.popup(
defineAsyncComponent(() => import("@/components/MkTooltip.vue")),
@ -94,6 +106,7 @@ function tooltipShow() {
);
}
function tooltipHide() {
if (!props.tooltips) return;
tooltipShowing.value = false;
}
</script>
@ -128,13 +141,21 @@ function tooltipHide() {
$thumbWidth: 20px;
> .body {
padding: 10px 12px;
background: var(--panel);
border: solid 1px var(--panel);
padding: 10px 0;
background: none;
border: none;
border-radius: 6px;
&.background {
padding: 10px 12px;
background: var(--panel);
border: solid 1px var(--panel);
}
> .container {
position: relative;
display: flex;
align-items: center;
height: $thumbHeight;
@mixin track {
@ -155,6 +176,7 @@ function tooltipHide() {
&:hover {
background: var(--accentLighten);
cursor: pointer;
}
}
> input {

View file

@ -38,6 +38,74 @@ export const FILE_TYPE_BROWSERSAFE = [
"audio/x-flac",
"audio/vnd.wave",
];
export const FILE_TYPE_TRACKER_MODULES = [
"audio/mod",
"audio/x-mod",
"audio/s3m",
"audio/x-s3m",
"audio/xm",
"audio/x-xm",
"audio/it",
"audio/x-it",
];
export const FILE_EXT_TRACKER_MODULES = [
"mptm",
"mod",
"s3m",
"xm",
"it",
"667",
"669",
"amf",
"ams",
"c67",
"dbm",
"digi",
"dmf",
"dsm",
"dsym",
"dtm",
"far",
"fmt",
"imf",
"ice",
"j2b",
"m15",
"mdl",
"med",
"mms",
"mt2",
"mtm",
"mus",
"nst",
"okt",
"plm",
"psm",
"pt36",
"ptm",
"sfx",
"sfx2",
"st26",
"stk",
"stm",
"stx",
"stp",
"symmod",
"gtk",
"gt2",
"ult",
"wow",
"xmf",
"gdm",
"mo3",
"oxm",
"umx",
"xpk",
"ppm",
"mmcmp",
];
/*
https://github.com/sindresorhus/file-type/blob/main/supported.js
https://github.com/sindresorhus/file-type/blob/main/core.js

View file

@ -0,0 +1,372 @@
import wasm from "libopenmpt-wasm";
const ChiptuneAudioContext = window.AudioContext;
export function ChiptuneJsConfig(repeatCount?: number, context?: AudioContext) {
this.repeatCount = repeatCount;
this.context = context;
}
ChiptuneJsConfig.prototype.constructor = ChiptuneJsConfig;
export function ChiptuneJsPlayer(config: object) {
this.libopenmpt = null;
this.config = config;
this.audioContext = config.context || new ChiptuneAudioContext();
this.context = this.audioContext.createGain();
this.currentPlayingNode = null;
this.handlers = [];
this.touchLocked = true;
this.volume = 1;
}
ChiptuneJsPlayer.prototype.constructor = ChiptuneJsPlayer;
ChiptuneJsPlayer.prototype.fireEvent = function (eventName: string, response) {
const handlers = this.handlers;
if (handlers.length > 0) {
for (const handler of handlers) {
if (handler.eventName === eventName) {
handler.handler(response);
}
}
}
};
ChiptuneJsPlayer.prototype.addHandler = function (
eventName: string,
handler: Function,
) {
this.handlers.push({ eventName, handler });
};
ChiptuneJsPlayer.prototype.clearHandlers = function () {
this.handlers = [];
};
ChiptuneJsPlayer.prototype.onEnded = function (handler: Function) {
this.addHandler("onEnded", handler);
};
ChiptuneJsPlayer.prototype.onError = function (handler: Function) {
this.addHandler("onError", handler);
};
ChiptuneJsPlayer.prototype.duration = function () {
return this.libopenmpt._openmpt_module_get_duration_seconds(
this.currentPlayingNode.modulePtr,
);
};
ChiptuneJsPlayer.prototype.position = function () {
return this.libopenmpt._openmpt_module_get_position_seconds(
this.currentPlayingNode.modulePtr,
);
};
ChiptuneJsPlayer.prototype.repeat = function (repeatCount: number) {
if (this.currentPlayingNode) {
this.libopenmpt._openmpt_module_set_repeat_count(
this.currentPlayingNode.modulePtr,
repeatCount,
);
}
};
ChiptuneJsPlayer.prototype.seek = function (position: number) {
if (this.currentPlayingNode) {
this.libopenmpt._openmpt_module_set_position_seconds(
this.currentPlayingNode.modulePtr,
position,
);
}
};
ChiptuneJsPlayer.prototype.metadata = function () {
const data = {};
const keys = this.libopenmpt
.UTF8ToString(
this.libopenmpt._openmpt_module_get_metadata_keys(
this.currentPlayingNode.modulePtr,
),
)
.split(";");
let keyNameBuffer = 0;
for (const key of keys) {
keyNameBuffer = this.libopenmpt._malloc(key.length + 1);
this.libopenmpt.stringToUTF8(key, keyNameBuffer);
data[key] = this.libopenmpt.UTF8ToString(
this.libopenmpt._openmpt_module_get_metadata(
this.currentPlayingNode.modulePtr,
keyNameBuffer,
),
);
this.libopenmpt._free(keyNameBuffer);
}
return data;
};
ChiptuneJsPlayer.prototype.unlock = function () {
const context = this.audioContext;
const buffer = context.createBuffer(1, 1, 22050);
const unlockSource = context.createBufferSource();
unlockSource.buffer = buffer;
unlockSource.connect(this.context);
this.context.connect(context.destination);
unlockSource.start(0);
this.touchLocked = false;
};
ChiptuneJsPlayer.prototype.load = function (input) {
return new Promise((resolve, reject) => {
if (this.touchLocked) {
this.unlock();
}
if (input instanceof File) {
const reader = new FileReader();
reader.onload = () => {
resolve(reader.result);
};
reader.readAsArrayBuffer(input);
} else {
window
.fetch(input)
.then((response) => {
response
.arrayBuffer()
.then((arrayBuffer) => {
resolve(arrayBuffer);
})
.catch((error) => {
reject(error);
});
})
.catch((error) => {
reject(error);
});
}
});
};
ChiptuneJsPlayer.prototype.play = async function (buffer: ArrayBuffer) {
this.unlock();
this.stop();
return this.createLibopenmptNode(buffer, this.buffer).then((processNode) => {
if (processNode === null) {
return;
}
this.libopenmpt._openmpt_module_set_repeat_count(
processNode.modulePtr,
this.config.repeatCount || 0,
);
this.currentPlayingNode = processNode;
processNode.connect(this.context);
this.context.connect(this.audioContext.destination);
});
};
ChiptuneJsPlayer.prototype.stop = function () {
if (this.currentPlayingNode != null) {
this.currentPlayingNode.disconnect();
this.currentPlayingNode.cleanup();
this.currentPlayingNode = null;
}
};
ChiptuneJsPlayer.prototype.togglePause = function () {
if (this.currentPlayingNode != null) {
this.currentPlayingNode.togglePause();
}
};
ChiptuneJsPlayer.prototype.getPattern = function () {
if (this.currentPlayingNode?.modulePtr) {
return this.libopenmpt._openmpt_module_get_current_pattern(
this.currentPlayingNode.modulePtr,
);
}
return 0;
};
ChiptuneJsPlayer.prototype.getRow = function () {
if (this.currentPlayingNode?.modulePtr) {
return this.libopenmpt._openmpt_module_get_current_row(
this.currentPlayingNode.modulePtr,
);
}
return 0;
};
ChiptuneJsPlayer.prototype.getNumPatterns = function () {
if (this.currentPlayingNode?.modulePtr) {
return this.libopenmpt._openmpt_module_get_num_patterns(
this.currentPlayingNode.modulePtr,
);
}
return 0;
};
ChiptuneJsPlayer.prototype.getPatternNumRows = function (pattern: number) {
if (this.currentPlayingNode?.modulePtr) {
return this.libopenmpt._openmpt_module_get_pattern_num_rows(
this.currentPlayingNode.modulePtr,
pattern,
);
}
return 0;
};
ChiptuneJsPlayer.prototype.getPatternRowChannel = function (
pattern: number,
row: number,
channel: number,
) {
if (this.currentPlayingNode?.modulePtr) {
return this.libopenmpt.UTF8ToString(
this.libopenmpt._openmpt_module_format_pattern_row_channel(
this.currentPlayingNode.modulePtr,
pattern,
row,
channel,
0,
true,
),
);
}
return "";
};
ChiptuneJsPlayer.prototype.createLibopenmptNode = async function (
buffer,
config: object,
) {
const maxFramesPerChunk = 4096;
const processNode = this.audioContext.createScriptProcessor(2048, 0, 2);
processNode.config = config;
processNode.player = this;
if (!this.libopenmpt) this.libopenmpt = await wasm();
const byteArray = new Int8Array(buffer);
const ptrToFile = this.libopenmpt._malloc(byteArray.byteLength);
this.libopenmpt.HEAPU8.set(byteArray, ptrToFile);
processNode.modulePtr = this.libopenmpt._openmpt_module_create_from_memory(
ptrToFile,
byteArray.byteLength,
0,
0,
0,
);
processNode.nbChannels = this.libopenmpt._openmpt_module_get_num_channels(
processNode.modulePtr,
);
processNode.patternIndex = -1;
processNode.paused = false;
processNode.leftBufferPtr = this.libopenmpt._malloc(4 * maxFramesPerChunk);
processNode.rightBufferPtr = this.libopenmpt._malloc(4 * maxFramesPerChunk);
processNode.cleanup = function () {
if (this.modulePtr !== 0) {
processNode.player.libopenmpt._openmpt_module_destroy(this.modulePtr);
this.modulePtr = 0;
}
if (this.leftBufferPtr !== 0) {
processNode.player.libopenmpt._free(this.leftBufferPtr);
this.leftBufferPtr = 0;
}
if (this.rightBufferPtr !== 0) {
processNode.player.libopenmpt._free(this.rightBufferPtr);
this.rightBufferPtr = 0;
}
};
processNode.stop = function () {
this.disconnect();
this.cleanup();
};
processNode.pause = function () {
this.paused = true;
};
processNode.unpause = function () {
this.paused = false;
};
processNode.togglePause = function () {
this.paused = !this.paused;
};
processNode.onaudioprocess = function (e) {
const outputL = e.outputBuffer.getChannelData(0);
const outputR = e.outputBuffer.getChannelData(1);
let framesToRender = outputL.length;
if (this.ModulePtr === 0) {
for (let i = 0; i < framesToRender; ++i) {
outputL[i] = 0;
outputR[i] = 0;
}
this.disconnect();
this.cleanup();
return;
}
if (this.paused) {
for (let i = 0; i < framesToRender; ++i) {
outputL[i] = 0;
outputR[i] = 0;
}
return;
}
let framesRendered = 0;
let ended = false;
let error = false;
const currentPattern =
processNode.player.libopenmpt._openmpt_module_get_current_pattern(
this.modulePtr,
);
const currentRow =
processNode.player.libopenmpt._openmpt_module_get_current_row(
this.modulePtr,
);
if (currentPattern !== this.patternIndex) {
processNode.player.fireEvent("onPatternChange");
}
processNode.player.fireEvent("onRowChange", { index: currentRow });
while (framesToRender > 0) {
const framesPerChunk = Math.min(framesToRender, maxFramesPerChunk);
const actualFramesPerChunk =
processNode.player.libopenmpt._openmpt_module_read_float_stereo(
this.modulePtr,
this.context.sampleRate,
framesPerChunk,
this.leftBufferPtr,
this.rightBufferPtr,
);
if (actualFramesPerChunk === 0) {
ended = true;
// modulePtr will be 0 on openmpt: error: openmpt_module_read_float_stereo: ERROR: module * not valid or other openmpt error
error = !this.modulePtr;
}
const rawAudioLeft = processNode.player.libopenmpt.HEAPF32.subarray(
this.leftBufferPtr / 4,
this.leftBufferPtr / 4 + actualFramesPerChunk,
);
const rawAudioRight = processNode.player.libopenmpt.HEAPF32.subarray(
this.rightBufferPtr / 4,
this.rightBufferPtr / 4 + actualFramesPerChunk,
);
for (let i = 0; i < actualFramesPerChunk; ++i) {
outputL[framesRendered + i] = rawAudioLeft[i];
outputR[framesRendered + i] = rawAudioRight[i];
}
for (let i = actualFramesPerChunk; i < framesPerChunk; ++i) {
outputL[framesRendered + i] = 0;
outputR[framesRendered + i] = 0;
}
framesToRender -= framesPerChunk;
framesRendered += framesPerChunk;
}
if (ended) {
this.disconnect();
this.cleanup();
error
? processNode.player.fireEvent("onError", { type: "openmpt" })
: processNode.player.fireEvent("onEnded");
}
};
return processNode;
};

File diff suppressed because it is too large Load diff