This commit is contained in:
syuilo 2024-08-26 15:28:10 +09:00
parent 583adec980
commit 048aa6f306
8 changed files with 171 additions and 53 deletions

View file

@ -20,7 +20,6 @@
"@twemoji/parser": "15.1.1",
"@vitejs/plugin-vue": "5.1.0",
"@vue/compiler-sfc": "3.4.37",
"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.11",
"astring": "1.8.6",
"buraha": "0.0.1",
"compare-versions": "6.1.1",
@ -30,7 +29,6 @@
"eventemitter3": "5.0.1",
"idb-keyval": "6.2.1",
"is-file-animated": "1.0.2",
"json5": "2.2.3",
"mfm-js": "0.24.0",
"misskey-js": "workspace:*",
"punycode": "2.3.1",

View file

@ -0,0 +1,85 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div :class="$style.root" :style="bg">
<img v-if="faviconUrl" :class="$style.icon" :src="faviconUrl"/>
<div :class="$style.name">{{ instance.name }}</div>
</div>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import { instanceName } from '@/config.js';
import { instance as Instance } from '@/instance.js';
import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js';
const props = defineProps<{
instance?: {
faviconUrl?: string | null
name?: string | null
themeColor?: string | null
}
}>();
// if no instance data is given, this is for the local instance
const instance = props.instance ?? {
name: instanceName,
themeColor: (document.querySelector('meta[name="theme-color-orig"]') as HTMLMetaElement).content,
};
const faviconUrl = computed(() => props.instance ? getProxiedImageUrlNullable(props.instance.faviconUrl, 'preview') : getProxiedImageUrlNullable(Instance.iconUrl, 'preview') ?? '/favicon.ico');
const themeColor = instance.themeColor ?? '#777777';
const bg = {
background: `linear-gradient(90deg, ${themeColor}, ${themeColor}00)`,
};
</script>
<style lang="scss" module>
$height: 2ex;
.root {
display: flex;
align-items: center;
height: $height;
border-radius: 4px 0 0 4px;
overflow: clip;
color: #fff;
text-shadow: /* .866 ≈ sin(60deg) */
1px 0 1px #000,
.866px .5px 1px #000,
.5px .866px 1px #000,
0 1px 1px #000,
-.5px .866px 1px #000,
-.866px .5px 1px #000,
-1px 0 1px #000,
-.866px -.5px 1px #000,
-.5px -.866px 1px #000,
0 -1px 1px #000,
.5px -.866px 1px #000,
.866px -.5px 1px #000;
mask-image: linear-gradient(90deg,
rgb(0,0,0),
rgb(0,0,0) calc(100% - 16px),
rgba(0,0,0,0) 100%
);
}
.icon {
height: $height;
flex-shrink: 0;
}
.name {
margin-left: 4px;
line-height: 1;
font-size: 0.9em;
font-weight: bold;
white-space: nowrap;
overflow: visible;
}
</style>

View file

@ -129,13 +129,16 @@ SPDX-License-Identifier: AGPL-3.0-only
import { computed, inject, ref } from 'vue';
import * as mfm from 'mfm-js';
import * as Misskey from 'misskey-js';
import EmMediaList from './EmMediaList.vue';
import EmMediaList from '@/components/EmMediaList.vue';
import EmNoteSub from '@/components/EmNoteSub.vue';
import EmNoteSimple from '@/components/EmNoteSimple.vue';
import EmReactionsViewer from '@/components/EmReactionsViewer.vue';
import EmCwButton from '@/components/EmCwButton.vue';
import EmPoll from '@/components/EmPoll.vue';
import EmInstanceTicker from '@/components/EmInstanceTicker.vue';
import EmA from '@/components/EmA.vue';
import EmAvatar from '@/components/EmAvatar.vue';
import EmTime from '@/components/EmTime.vue';
import { userPage } from '@/utils.js';
import { notePage } from '@/utils.js';
import { i18n } from '@/i18n.js';

View file

@ -9,4 +9,5 @@ import { misskeyApi } from '@/misskey-api.js';
export const instance = {
iconUrl: 'TODO',
mediaProxy: 'TODO',
};

View file

@ -0,0 +1,53 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { query } from './url.js';
import { url } from '@/config.js';
import { instance } from '@/instance.js';
export function getProxiedImageUrl(imageUrl: string, type?: 'preview' | 'emoji' | 'avatar', mustOrigin = false, noFallback = false): string {
const localProxy = `${url}/proxy`;
if (imageUrl.startsWith(instance.mediaProxy + '/') || imageUrl.startsWith('/proxy/') || imageUrl.startsWith(localProxy + '/')) {
// もう既にproxyっぽそうだったらurlを取り出す
imageUrl = (new URL(imageUrl)).searchParams.get('url') ?? imageUrl;
}
return `${mustOrigin ? localProxy : instance.mediaProxy}/${
type === 'preview' ? 'preview.webp'
: 'image.webp'
}?${query({
url: imageUrl,
...(!noFallback ? { 'fallback': '1' } : {}),
...(type ? { [type]: '1' } : {}),
...(mustOrigin ? { origin: '1' } : {}),
})}`;
}
export function getProxiedImageUrlNullable(imageUrl: string | null | undefined, type?: 'preview'): string | null {
if (imageUrl == null) return null;
return getProxiedImageUrl(imageUrl, type);
}
export function getStaticImageUrl(baseUrl: string): string {
const u = baseUrl.startsWith('http') ? new URL(baseUrl) : new URL(baseUrl, url);
if (u.href.startsWith(`${url}/emoji/`)) {
// もう既にemojiっぽそうだったらsearchParams付けるだけ
u.searchParams.set('static', '1');
return u.href;
}
if (u.href.startsWith(instance.mediaProxy + '/')) {
// もう既にproxyっぽそうだったらsearchParams付けるだけ
u.searchParams.set('static', '1');
return u.href;
}
return `${instance.mediaProxy}/static.webp?${query({
url: u.href,
static: '1',
})}`;
}

View file

@ -0,0 +1,28 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
/* obj
* 1.
* 2. undefinedの時はクエリを付けない
* new URLSearchParams(obj)
*/
export function query(obj: Record<string, any>): string {
const params = Object.entries(obj)
.filter(([, v]) => Array.isArray(v) ? v.length : v !== undefined)
.reduce((a, [k, v]) => (a[k] = v, a), {} as Record<string, any>);
return Object.entries(params)
.map((p) => `${p[0]}=${encodeURIComponent(p[1])}`)
.join('&');
}
export function appendQuery(url: string, query: string): string {
return `${url}${/\?/.test(url) ? url.endsWith('?') ? '' : '&' : '?'}${query}`;
}
export function extractDomain(url: string) {
const match = url.match(/^(?:https?:)?(?:\/\/)?(?:[^@\n]+@)?([^:\/\n]+)/im);
return match ? match[1] : null;
}

View file

@ -5,7 +5,6 @@ import { type UserConfig, defineConfig } from 'vite';
import locales from '../../locales/index.js';
import meta from '../../package.json';
import packageInfo from './package.json' with { type: 'json' };
import pluginJson5 from './vite.json5.js';
const extensions = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.json', '.json5', '.svg', '.sass', '.scss', '.css', '.vue'];
@ -67,7 +66,6 @@ export function getConfig(): UserConfig {
plugins: [
pluginVue(),
pluginJson5(),
],
resolve: {

View file

@ -1,48 +0,0 @@
// Original: https://github.com/rollup/plugins/tree/8835dd2aed92f408d7dc72d7cc25a9728e16face/packages/json
import JSON5 from 'json5';
import { Plugin } from 'rollup';
import { createFilter, dataToEsm } from '@rollup/pluginutils';
import { RollupJsonOptions } from '@rollup/plugin-json';
// json5 extends SyntaxError with additional fields (without subclassing)
// https://github.com/json5/json5/blob/de344f0619bda1465a6e25c76f1c0c3dda8108d9/lib/parse.js#L1111-L1112
interface Json5SyntaxError extends SyntaxError {
lineNumber: number;
columnNumber: number;
}
export default function json5(options: RollupJsonOptions = {}): Plugin {
const filter = createFilter(options.include, options.exclude);
const indent = 'indent' in options ? options.indent : '\t';
return {
name: 'json5',
// eslint-disable-next-line no-shadow
transform(json, id) {
if (id.slice(-6) !== '.json5' || !filter(id)) return null;
try {
const parsed = JSON5.parse(json);
return {
code: dataToEsm(parsed, {
preferConst: options.preferConst,
compact: options.compact,
namedExports: options.namedExports,
indent,
}),
map: { mappings: '' },
};
} catch (err) {
if (!(err instanceof SyntaxError)) {
throw err;
}
const message = 'Could not parse JSON5 file';
const { lineNumber, columnNumber } = err as Json5SyntaxError;
this.warn({ message, id, loc: { line: lineNumber, column: columnNumber } });
return null;
}
},
};
}