Merge tag '13.13.2' into merge-upstream
This commit is contained in:
@@ -56,14 +56,15 @@ module.exports = {
|
||||
'vue/require-v-for-key': 'warn',
|
||||
'vue/no-unused-components': 'warn',
|
||||
'vue/no-unused-vars': 'warn',
|
||||
'vue/no-dupe-keys': 'warn',
|
||||
'vue/valid-v-for': 'warn',
|
||||
'vue/return-in-computed-property': 'warn',
|
||||
'vue/no-setup-props-destructure': 'warn',
|
||||
'vue/max-attributes-per-line': 'off',
|
||||
'vue/html-self-closing': 'off',
|
||||
'vue/singleline-html-element-content-newline': 'off',
|
||||
// (vue/vue3-recommended disabled the autofix for Vue 2 compatibility)
|
||||
'vue/v-on-event-hyphenation': ['warn', 'always', { autofix: true }],
|
||||
'vue/v-on-event-hyphenation': ['error', 'never', { autofix: true }],
|
||||
'vue/attribute-hyphenation': ['error', 'never'],
|
||||
},
|
||||
globals: {
|
||||
// Node.js
|
||||
|
@@ -397,6 +397,7 @@ function toStories(component: string): string {
|
||||
Promise.all([
|
||||
glob('src/components/global/*.vue'),
|
||||
glob('src/components/Mk{A,B}*.vue'),
|
||||
glob('src/components/MkDigitalClock.vue'),
|
||||
glob('src/components/MkGalleryPostPreview.vue'),
|
||||
glob('src/components/MkSignupServerRules.vue'),
|
||||
glob('src/components/MkUserSetupDialog.vue'),
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<link rel="preload" href="https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/about-icon.png?raw=true" as="image" type="image/png" crossorigin="anonymous">
|
||||
<link rel="preload" href="https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/fedi.jpg?raw=true" as="image" type="image/jpeg" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="https://unpkg.com/@tabler/icons-webfont@2.12.0/tabler-icons.min.css">
|
||||
<link rel="stylesheet" href="https://unpkg.com/@tabler/icons-webfont@2.21.0/tabler-icons.min.css">
|
||||
<link rel="stylesheet" href="https://unpkg.com/@fontsource/m-plus-rounded-1c/index.css">
|
||||
<style>
|
||||
html {
|
||||
|
@@ -0,0 +1,597 @@
|
||||
import { parse } from 'acorn';
|
||||
import { generate } from 'astring';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { normalizeClass, unwindCssModuleClassName } from './rollup-plugin-unwind-css-module-class-name';
|
||||
import type * as estree from 'estree';
|
||||
|
||||
function parseExpression(code: string): estree.Expression {
|
||||
const program = parse(code, { ecmaVersion: 'latest', sourceType: 'module' }) as unknown as estree.Program;
|
||||
const statement = program.body[0] as estree.ExpressionStatement;
|
||||
return statement.expression;
|
||||
}
|
||||
|
||||
describe(normalizeClass.name, () => {
|
||||
it('should normalize string', () => {
|
||||
expect(normalizeClass(parseExpression('"a b c"'))).toBe('a b c');
|
||||
});
|
||||
it('should trim redundant spaces', () => {
|
||||
expect(normalizeClass(parseExpression('" a b c "'))).toBe('a b c');
|
||||
});
|
||||
it('should ignore undefined', () => {
|
||||
expect(normalizeClass(parseExpression('undefined'))).toBe('');
|
||||
});
|
||||
it('should ignore non string literals', () => {
|
||||
expect(normalizeClass(parseExpression('0'))).toBe('');
|
||||
expect(normalizeClass(parseExpression('true'))).toBe('');
|
||||
expect(normalizeClass(parseExpression('null'))).toBe('');
|
||||
expect(normalizeClass(parseExpression('/I.D/'))).toBe('');
|
||||
});
|
||||
it('should not normalize identifiers', () => {
|
||||
expect(normalizeClass(parseExpression('EScape'))).toBeNull();
|
||||
});
|
||||
it('should normalize recursively array', () => {
|
||||
expect(normalizeClass(parseExpression('["from", ...["Utopia"]]'))).toBe('from Utopia');
|
||||
expect(normalizeClass(parseExpression('["from", ...[Utopia]]'))).toBeNull();
|
||||
});
|
||||
it('should normalize recursively template literal', () => {
|
||||
expect(normalizeClass(parseExpression('`name ${"shiho"} code ${33}`'))).toBe('name shiho code');
|
||||
expect(normalizeClass(parseExpression('`name ${shiho.name} code ${33}`'))).toBeNull();
|
||||
});
|
||||
it('should normalize recursively binary expression', () => {
|
||||
expect(normalizeClass(parseExpression('"mirage" + "mirror"'))).toBe('miragemirror');
|
||||
expect(normalizeClass(parseExpression('"mirage" + mirror'))).toBeNull();
|
||||
});
|
||||
it('should normalize recursively object expression', () => {
|
||||
expect(normalizeClass(parseExpression('({ a: true, b: "c" })'))).toBe('a b');
|
||||
expect(normalizeClass(parseExpression('({ a: false, b: "c" })'))).toBe('b');
|
||||
expect(normalizeClass(parseExpression('({ a: true, b: c })'))).toBeNull();
|
||||
expect(normalizeClass(parseExpression('({ a: true, b: "c", ...({ d: true }) })'))).toBe('a b d');
|
||||
expect(normalizeClass(parseExpression('({ a: true, [b]: "c" })'))).toBeNull();
|
||||
expect(normalizeClass(parseExpression('({ a: true, b: false, c: !false, d: !!0 })'))).toBe('a c');
|
||||
});
|
||||
});
|
||||
|
||||
it('Composition API (standard)', () => {
|
||||
const ast = parse(`
|
||||
import { c as api, d as defaultStore, i as i18n, aD as notePage, bN as ImgWithBlurhash, bY as getStaticImageUrl, _ as _export_sfc } from './app-!~{001}~.js';
|
||||
import { M as MkContainer } from './MkContainer-!~{03M}~.js';
|
||||
import { b as defineComponent, a as ref, e as onMounted, z as resolveComponent, g as openBlock, h as createBlock, i as withCtx, K as createTextVNode, E as toDisplayString, u as unref, l as createBaseVNode, q as normalizeClass, B as createCommentVNode, k as createElementBlock, F as Fragment, C as renderList, A as createVNode } from './vue-!~{002}~.js';
|
||||
import './photoswipe-!~{003}~.js';
|
||||
|
||||
const _hoisted_1 = /* @__PURE__ */ createBaseVNode("i", { class: "ti ti-photo" }, null, -1);
|
||||
const _sfc_main = /* @__PURE__ */ defineComponent({
|
||||
__name: "index.photos",
|
||||
props: {
|
||||
user: {}
|
||||
},
|
||||
setup(__props) {
|
||||
const props = __props;
|
||||
let fetching = ref(true);
|
||||
let images = ref([]);
|
||||
function thumbnail(image) {
|
||||
return defaultStore.state.disableShowingAnimatedImages ? getStaticImageUrl(image.url) : image.thumbnailUrl;
|
||||
}
|
||||
onMounted(() => {
|
||||
const image = [
|
||||
"image/jpeg",
|
||||
"image/webp",
|
||||
"image/avif",
|
||||
"image/png",
|
||||
"image/gif",
|
||||
"image/apng",
|
||||
"image/vnd.mozilla.apng"
|
||||
];
|
||||
api("users/notes", {
|
||||
userId: props.user.id,
|
||||
fileType: image,
|
||||
excludeNsfw: defaultStore.state.nsfw !== "ignore",
|
||||
limit: 10
|
||||
}).then((notes) => {
|
||||
for (const note of notes) {
|
||||
for (const file of note.files) {
|
||||
images.value.push({
|
||||
note,
|
||||
file
|
||||
});
|
||||
}
|
||||
}
|
||||
fetching.value = false;
|
||||
});
|
||||
});
|
||||
return (_ctx, _cache) => {
|
||||
const _component_MkLoading = resolveComponent("MkLoading");
|
||||
const _component_MkA = resolveComponent("MkA");
|
||||
return openBlock(), createBlock(MkContainer, {
|
||||
"max-height": 300,
|
||||
foldable: true
|
||||
}, {
|
||||
icon: withCtx(() => [
|
||||
_hoisted_1
|
||||
]),
|
||||
header: withCtx(() => [
|
||||
createTextVNode(toDisplayString(unref(i18n).ts.images), 1)
|
||||
]),
|
||||
default: withCtx(() => [
|
||||
createBaseVNode("div", {
|
||||
class: normalizeClass(_ctx.$style.root)
|
||||
}, [
|
||||
unref(fetching) ? (openBlock(), createBlock(_component_MkLoading, { key: 0 })) : createCommentVNode("", true),
|
||||
!unref(fetching) && unref(images).length > 0 ? (openBlock(), createElementBlock("div", {
|
||||
key: 1,
|
||||
class: normalizeClass(_ctx.$style.stream)
|
||||
}, [
|
||||
(openBlock(true), createElementBlock(Fragment, null, renderList(unref(images), (image) => {
|
||||
return openBlock(), createBlock(_component_MkA, {
|
||||
key: image.note.id + image.file.id,
|
||||
class: normalizeClass(_ctx.$style.img),
|
||||
to: unref(notePage)(image.note)
|
||||
}, {
|
||||
default: withCtx(() => [
|
||||
createVNode(ImgWithBlurhash, {
|
||||
hash: image.file.blurhash,
|
||||
src: thumbnail(image.file),
|
||||
title: image.file.name
|
||||
}, null, 8, ["hash", "src", "title"])
|
||||
]),
|
||||
_: 2
|
||||
}, 1032, ["class", "to"]);
|
||||
}), 128))
|
||||
], 2)) : createCommentVNode("", true),
|
||||
!unref(fetching) && unref(images).length == 0 ? (openBlock(), createElementBlock("p", {
|
||||
key: 2,
|
||||
class: normalizeClass(_ctx.$style.empty)
|
||||
}, toDisplayString(unref(i18n).ts.nothing), 3)) : createCommentVNode("", true)
|
||||
], 2)
|
||||
]),
|
||||
_: 1
|
||||
});
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const root = "xenMW";
|
||||
const stream = "xaZzf";
|
||||
const img = "xtA8t";
|
||||
const empty = "xhYKj";
|
||||
const style0 = {
|
||||
root: root,
|
||||
stream: stream,
|
||||
img: img,
|
||||
empty: empty
|
||||
};
|
||||
|
||||
const cssModules = {
|
||||
"$style": style0
|
||||
};
|
||||
const index_photos = /* @__PURE__ */ _export_sfc(_sfc_main, [["__cssModules", cssModules]]);
|
||||
|
||||
export { index_photos as default };
|
||||
`.slice(1), { ecmaVersion: 'latest', sourceType: 'module' });
|
||||
unwindCssModuleClassName(ast);
|
||||
expect(generate(ast)).toBe(`
|
||||
import {c as api, d as defaultStore, i as i18n, aD as notePage, bN as ImgWithBlurhash, bY as getStaticImageUrl, _ as _export_sfc} from './app-!~{001}~.js';
|
||||
import {M as MkContainer} from './MkContainer-!~{03M}~.js';
|
||||
import {b as defineComponent, a as ref, e as onMounted, z as resolveComponent, g as openBlock, h as createBlock, i as withCtx, K as createTextVNode, E as toDisplayString, u as unref, l as createBaseVNode, q as normalizeClass, B as createCommentVNode, k as createElementBlock, F as Fragment, C as renderList, A as createVNode} from './vue-!~{002}~.js';
|
||||
import './photoswipe-!~{003}~.js';
|
||||
const _hoisted_1 = createBaseVNode("i", {
|
||||
class: "ti ti-photo"
|
||||
}, null, -1);
|
||||
const _sfc_main = defineComponent({
|
||||
__name: "index.photos",
|
||||
props: {
|
||||
user: {}
|
||||
},
|
||||
setup(__props) {
|
||||
const props = __props;
|
||||
let fetching = ref(true);
|
||||
let images = ref([]);
|
||||
function thumbnail(image) {
|
||||
return defaultStore.state.disableShowingAnimatedImages ? getStaticImageUrl(image.url) : image.thumbnailUrl;
|
||||
}
|
||||
onMounted(() => {
|
||||
const image = ["image/jpeg", "image/webp", "image/avif", "image/png", "image/gif", "image/apng", "image/vnd.mozilla.apng"];
|
||||
api("users/notes", {
|
||||
userId: props.user.id,
|
||||
fileType: image,
|
||||
excludeNsfw: defaultStore.state.nsfw !== "ignore",
|
||||
limit: 10
|
||||
}).then(notes => {
|
||||
for (const note of notes) {
|
||||
for (const file of note.files) {
|
||||
images.value.push({
|
||||
note,
|
||||
file
|
||||
});
|
||||
}
|
||||
}
|
||||
fetching.value = false;
|
||||
});
|
||||
});
|
||||
return (_ctx, _cache) => {
|
||||
const _component_MkLoading = resolveComponent("MkLoading");
|
||||
const _component_MkA = resolveComponent("MkA");
|
||||
return (openBlock(), createBlock(MkContainer, {
|
||||
"max-height": 300,
|
||||
foldable: true
|
||||
}, {
|
||||
icon: withCtx(() => [_hoisted_1]),
|
||||
header: withCtx(() => [createTextVNode(toDisplayString(unref(i18n).ts.images), 1)]),
|
||||
default: withCtx(() => [createBaseVNode("div", {
|
||||
class: "xenMW"
|
||||
}, [unref(fetching) ? (openBlock(), createBlock(_component_MkLoading, {
|
||||
key: 0
|
||||
})) : createCommentVNode("", true), !unref(fetching) && unref(images).length > 0 ? (openBlock(), createElementBlock("div", {
|
||||
key: 1,
|
||||
class: "xaZzf"
|
||||
}, [(openBlock(true), createElementBlock(Fragment, null, renderList(unref(images), image => {
|
||||
return (openBlock(), createBlock(_component_MkA, {
|
||||
key: image.note.id + image.file.id,
|
||||
class: "xtA8t",
|
||||
to: unref(notePage)(image.note)
|
||||
}, {
|
||||
default: withCtx(() => [createVNode(ImgWithBlurhash, {
|
||||
hash: image.file.blurhash,
|
||||
src: thumbnail(image.file),
|
||||
title: image.file.name
|
||||
}, null, 8, ["hash", "src", "title"])]),
|
||||
_: 2
|
||||
}, 1032, ["class", "to"]));
|
||||
}), 128))], 2)) : createCommentVNode("", true), !unref(fetching) && unref(images).length == 0 ? (openBlock(), createElementBlock("p", {
|
||||
key: 2,
|
||||
class: "xhYKj"
|
||||
}, toDisplayString(unref(i18n).ts.nothing), 3)) : createCommentVNode("", true)], 2)]),
|
||||
_: 1
|
||||
}));
|
||||
};
|
||||
}
|
||||
});
|
||||
const root = "xenMW";
|
||||
const stream = "xaZzf";
|
||||
const img = "xtA8t";
|
||||
const empty = "xhYKj";
|
||||
const style0 = {
|
||||
root: root,
|
||||
stream: stream,
|
||||
img: img,
|
||||
empty: empty
|
||||
};
|
||||
const cssModules = {
|
||||
"$style": style0
|
||||
};
|
||||
const index_photos = _sfc_main;
|
||||
export {index_photos as default};
|
||||
`.slice(1));
|
||||
});
|
||||
|
||||
it('Composition API (with `useCssModule()`)', () => {
|
||||
const ast = parse(`
|
||||
import { a7 as getCurrentInstance, b as defineComponent, G as useCssModule, a1 as h, H as TransitionGroup } from './!~{002}~.js';
|
||||
import { d as defaultStore, aK as toast, b5 as MkAd, i as i18n, _ as _export_sfc } from './app-!~{001}~.js';
|
||||
|
||||
function isDebuggerEnabled(id) {
|
||||
try {
|
||||
return localStorage.getItem(\`DEBUG_\${id}\`) !== null;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
function stackTraceInstances() {
|
||||
let instance = getCurrentInstance();
|
||||
const stack = [];
|
||||
while (instance) {
|
||||
stack.push(instance);
|
||||
instance = instance.parent;
|
||||
}
|
||||
return stack;
|
||||
}
|
||||
|
||||
const _sfc_main = defineComponent({
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
direction: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: "down"
|
||||
},
|
||||
reversed: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
noGap: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
ad: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
setup(props, { slots, expose }) {
|
||||
const $style = useCssModule();
|
||||
function getDateText(time) {
|
||||
const date = new Date(time).getDate();
|
||||
const month = new Date(time).getMonth() + 1;
|
||||
return i18n.t("monthAndDay", {
|
||||
month: month.toString(),
|
||||
day: date.toString()
|
||||
});
|
||||
}
|
||||
if (props.items.length === 0)
|
||||
return;
|
||||
const renderChildrenImpl = () => props.items.map((item, i) => {
|
||||
if (!slots || !slots.default)
|
||||
return;
|
||||
const el = slots.default({
|
||||
item
|
||||
})[0];
|
||||
if (el.key == null && item.id)
|
||||
el.key = item.id;
|
||||
if (i !== props.items.length - 1 && new Date(item.createdAt).getDate() !== new Date(props.items[i + 1].createdAt).getDate()) {
|
||||
const separator = h("div", {
|
||||
class: $style["separator"],
|
||||
key: item.id + ":separator"
|
||||
}, h("p", {
|
||||
class: $style["date"]
|
||||
}, [
|
||||
h("span", {
|
||||
class: $style["date-1"]
|
||||
}, [
|
||||
h("i", {
|
||||
class: \`ti ti-chevron-up \${$style["date-1-icon"]}\`
|
||||
}),
|
||||
getDateText(item.createdAt)
|
||||
]),
|
||||
h("span", {
|
||||
class: $style["date-2"]
|
||||
}, [
|
||||
getDateText(props.items[i + 1].createdAt),
|
||||
h("i", {
|
||||
class: \`ti ti-chevron-down \${$style["date-2-icon"]}\`
|
||||
})
|
||||
])
|
||||
]));
|
||||
return [el, separator];
|
||||
} else {
|
||||
if (props.ad && item._shouldInsertAd_) {
|
||||
return [h(MkAd, {
|
||||
key: item.id + ":ad",
|
||||
prefer: ["horizontal", "horizontal-big"]
|
||||
}), el];
|
||||
} else {
|
||||
return el;
|
||||
}
|
||||
}
|
||||
});
|
||||
const renderChildren = () => {
|
||||
const children = renderChildrenImpl();
|
||||
if (isDebuggerEnabled(6864)) {
|
||||
const nodes = children.flatMap((node) => node ?? []);
|
||||
const keys = new Set(nodes.map((node) => node.key));
|
||||
if (keys.size !== nodes.length) {
|
||||
const id = crypto.randomUUID();
|
||||
const instances = stackTraceInstances();
|
||||
toast(instances.reduce((a, c) => \`\${a} at \${c.type.name}\`, \`[DEBUG_6864 (\${id})]: \${nodes.length - keys.size} duplicated keys found\`));
|
||||
console.warn({ id, debugId: 6864, stack: instances });
|
||||
}
|
||||
}
|
||||
return children;
|
||||
};
|
||||
function onBeforeLeave(el) {
|
||||
el.style.top = \`\${el.offsetTop}px\`;
|
||||
el.style.left = \`\${el.offsetLeft}px\`;
|
||||
}
|
||||
function onLeaveCanceled(el) {
|
||||
el.style.top = "";
|
||||
el.style.left = "";
|
||||
}
|
||||
return () => h(
|
||||
defaultStore.state.animation ? TransitionGroup : "div",
|
||||
{
|
||||
class: {
|
||||
[$style["date-separated-list"]]: true,
|
||||
[$style["date-separated-list-nogap"]]: props.noGap,
|
||||
[$style["reversed"]]: props.reversed,
|
||||
[$style["direction-down"]]: props.direction === "down",
|
||||
[$style["direction-up"]]: props.direction === "up"
|
||||
},
|
||||
...defaultStore.state.animation ? {
|
||||
name: "list",
|
||||
tag: "div",
|
||||
onBeforeLeave,
|
||||
onLeaveCanceled
|
||||
} : {}
|
||||
},
|
||||
{ default: renderChildren }
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const reversed = "xxiZh";
|
||||
const separator = "xxeDx";
|
||||
const date = "xxawD";
|
||||
const style0 = {
|
||||
"date-separated-list": "xfKPa",
|
||||
"date-separated-list-nogap": "xf9zr",
|
||||
"direction-up": "x7AeO",
|
||||
"direction-down": "xBIqc",
|
||||
reversed: reversed,
|
||||
separator: separator,
|
||||
date: date,
|
||||
"date-1": "xwtmh",
|
||||
"date-1-icon": "xsNPa",
|
||||
"date-2": "x1xvw",
|
||||
"date-2-icon": "x9ZiG"
|
||||
};
|
||||
|
||||
const cssModules = {
|
||||
"$style": style0
|
||||
};
|
||||
const MkDateSeparatedList = /* @__PURE__ */ _export_sfc(_sfc_main, [["__cssModules", cssModules]]);
|
||||
|
||||
export { MkDateSeparatedList as M };
|
||||
`.slice(1), { ecmaVersion: 'latest', sourceType: 'module' });
|
||||
unwindCssModuleClassName(ast);
|
||||
expect(generate(ast)).toBe(`
|
||||
import {a7 as getCurrentInstance, b as defineComponent, G as useCssModule, a1 as h, H as TransitionGroup} from './!~{002}~.js';
|
||||
import {d as defaultStore, aK as toast, b5 as MkAd, i as i18n, _ as _export_sfc} from './app-!~{001}~.js';
|
||||
function isDebuggerEnabled(id) {
|
||||
try {
|
||||
return localStorage.getItem(\`DEBUG_\${id}\`) !== null;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
function stackTraceInstances() {
|
||||
let instance = getCurrentInstance();
|
||||
const stack = [];
|
||||
while (instance) {
|
||||
stack.push(instance);
|
||||
instance = instance.parent;
|
||||
}
|
||||
return stack;
|
||||
}
|
||||
const _sfc_main = defineComponent({
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
direction: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: "down"
|
||||
},
|
||||
reversed: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
noGap: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
ad: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
setup(props, {slots, expose}) {
|
||||
const $style = useCssModule();
|
||||
function getDateText(time) {
|
||||
const date = new Date(time).getDate();
|
||||
const month = new Date(time).getMonth() + 1;
|
||||
return i18n.t("monthAndDay", {
|
||||
month: month.toString(),
|
||||
day: date.toString()
|
||||
});
|
||||
}
|
||||
if (props.items.length === 0) return;
|
||||
const renderChildrenImpl = () => props.items.map((item, i) => {
|
||||
if (!slots || !slots.default) return;
|
||||
const el = slots.default({
|
||||
item
|
||||
})[0];
|
||||
if (el.key == null && item.id) el.key = item.id;
|
||||
if (i !== props.items.length - 1 && new Date(item.createdAt).getDate() !== new Date(props.items[i + 1].createdAt).getDate()) {
|
||||
const separator = h("div", {
|
||||
class: $style["separator"],
|
||||
key: item.id + ":separator"
|
||||
}, h("p", {
|
||||
class: $style["date"]
|
||||
}, [h("span", {
|
||||
class: $style["date-1"]
|
||||
}, [h("i", {
|
||||
class: \`ti ti-chevron-up \${$style["date-1-icon"]}\`
|
||||
}), getDateText(item.createdAt)]), h("span", {
|
||||
class: $style["date-2"]
|
||||
}, [getDateText(props.items[i + 1].createdAt), h("i", {
|
||||
class: \`ti ti-chevron-down \${$style["date-2-icon"]}\`
|
||||
})])]));
|
||||
return [el, separator];
|
||||
} else {
|
||||
if (props.ad && item._shouldInsertAd_) {
|
||||
return [h(MkAd, {
|
||||
key: item.id + ":ad",
|
||||
prefer: ["horizontal", "horizontal-big"]
|
||||
}), el];
|
||||
} else {
|
||||
return el;
|
||||
}
|
||||
}
|
||||
});
|
||||
const renderChildren = () => {
|
||||
const children = renderChildrenImpl();
|
||||
if (isDebuggerEnabled(6864)) {
|
||||
const nodes = children.flatMap(node => node ?? []);
|
||||
const keys = new Set(nodes.map(node => node.key));
|
||||
if (keys.size !== nodes.length) {
|
||||
const id = crypto.randomUUID();
|
||||
const instances = stackTraceInstances();
|
||||
toast(instances.reduce((a, c) => \`\${a} at \${c.type.name}\`, \`[DEBUG_6864 (\${id})]: \${nodes.length - keys.size} duplicated keys found\`));
|
||||
console.warn({
|
||||
id,
|
||||
debugId: 6864,
|
||||
stack: instances
|
||||
});
|
||||
}
|
||||
}
|
||||
return children;
|
||||
};
|
||||
function onBeforeLeave(el) {
|
||||
el.style.top = \`\${el.offsetTop}px\`;
|
||||
el.style.left = \`\${el.offsetLeft}px\`;
|
||||
}
|
||||
function onLeaveCanceled(el) {
|
||||
el.style.top = "";
|
||||
el.style.left = "";
|
||||
}
|
||||
return () => h(defaultStore.state.animation ? TransitionGroup : "div", {
|
||||
class: {
|
||||
[$style["date-separated-list"]]: true,
|
||||
[$style["date-separated-list-nogap"]]: props.noGap,
|
||||
[$style["reversed"]]: props.reversed,
|
||||
[$style["direction-down"]]: props.direction === "down",
|
||||
[$style["direction-up"]]: props.direction === "up"
|
||||
},
|
||||
...defaultStore.state.animation ? {
|
||||
name: "list",
|
||||
tag: "div",
|
||||
onBeforeLeave,
|
||||
onLeaveCanceled
|
||||
} : {}
|
||||
}, {
|
||||
default: renderChildren
|
||||
});
|
||||
}
|
||||
});
|
||||
const reversed = "xxiZh";
|
||||
const separator = "xxeDx";
|
||||
const date = "xxawD";
|
||||
const style0 = {
|
||||
"date-separated-list": "xfKPa",
|
||||
"date-separated-list-nogap": "xf9zr",
|
||||
"direction-up": "x7AeO",
|
||||
"direction-down": "xBIqc",
|
||||
reversed: reversed,
|
||||
separator: separator,
|
||||
date: date,
|
||||
"date-1": "xwtmh",
|
||||
"date-1-icon": "xsNPa",
|
||||
"date-2": "x1xvw",
|
||||
"date-2-icon": "x9ZiG"
|
||||
};
|
||||
const cssModules = {
|
||||
"$style": style0
|
||||
};
|
||||
const MkDateSeparatedList = _export_sfc(_sfc_main, [["__cssModules", cssModules]]);
|
||||
export {MkDateSeparatedList as M};
|
||||
`.slice(1));
|
||||
});
|
@@ -0,0 +1,275 @@
|
||||
import { generate } from 'astring';
|
||||
import * as estree from 'estree';
|
||||
import { walk } from '../node_modules/estree-walker/src/index.js';
|
||||
import type * as estreeWalker from 'estree-walker';
|
||||
import type { Plugin } from 'vite';
|
||||
|
||||
function isFalsyIdentifier(identifier: estree.Identifier): boolean {
|
||||
return identifier.name === 'undefined' || identifier.name === 'NaN';
|
||||
}
|
||||
|
||||
function normalizeClassWalker(tree: estree.Node): string | null {
|
||||
if (tree.type === 'Identifier') return isFalsyIdentifier(tree) ? '' : null;
|
||||
if (tree.type === 'Literal') return typeof tree.value === 'string' ? tree.value : '';
|
||||
if (tree.type === 'BinaryExpression') {
|
||||
if (tree.operator !== '+') return null;
|
||||
const left = normalizeClassWalker(tree.left);
|
||||
const right = normalizeClassWalker(tree.right);
|
||||
if (left === null || right === null) return null;
|
||||
return `${left}${right}`;
|
||||
}
|
||||
if (tree.type === 'TemplateLiteral') {
|
||||
if (tree.expressions.some((x) => x.type !== 'Literal' && (x.type !== 'Identifier' || !isFalsyIdentifier(x)))) return null;
|
||||
return tree.quasis.reduce((a, c, i) => {
|
||||
const v = i === tree.quasis.length - 1 ? '' : (tree.expressions[i] as Partial<estree.Literal>).value;
|
||||
return a + c.value.raw + (typeof v === 'string' ? v : '');
|
||||
}, '');
|
||||
}
|
||||
if (tree.type === 'ArrayExpression') {
|
||||
const values = tree.elements.map((treeNode) => {
|
||||
if (treeNode === null) return '';
|
||||
if (treeNode.type === 'SpreadElement') return normalizeClassWalker(treeNode.argument);
|
||||
return normalizeClassWalker(treeNode);
|
||||
});
|
||||
if (values.some((x) => x === null)) return null;
|
||||
return values.join(' ');
|
||||
}
|
||||
if (tree.type === 'ObjectExpression') {
|
||||
const values = tree.properties.map((treeNode) => {
|
||||
if (treeNode.type === 'SpreadElement') return normalizeClassWalker(treeNode.argument);
|
||||
let x = treeNode.value;
|
||||
let inveted = false;
|
||||
while (x.type === 'UnaryExpression' && x.operator === '!') {
|
||||
x = x.argument;
|
||||
inveted = !inveted;
|
||||
}
|
||||
if (x.type === 'Literal') {
|
||||
if (inveted === !x.value) {
|
||||
return treeNode.key.type === 'Identifier' ? treeNode.computed ? null : treeNode.key.name : treeNode.key.type === 'Literal' ? treeNode.key.value : '';
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
if (x.type === 'Identifier') {
|
||||
if (inveted !== isFalsyIdentifier(x)) {
|
||||
return '';
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
if (values.some((x) => x === null)) return null;
|
||||
return values.join(' ');
|
||||
}
|
||||
console.error(`Unexpected node type: ${tree.type}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
export function normalizeClass(tree: estree.Node): string | null {
|
||||
const walked = normalizeClassWalker(tree);
|
||||
return walked && walked.replace(/^\s+|\s+(?=\s)|\s+$/g, '');
|
||||
}
|
||||
|
||||
export function unwindCssModuleClassName(ast: estree.Node): void {
|
||||
(walk as typeof estreeWalker.walk)(ast, {
|
||||
enter(node, parent): void {
|
||||
if (parent?.type !== 'Program') return;
|
||||
if (node.type !== 'VariableDeclaration') return;
|
||||
if (node.declarations.length !== 1) return;
|
||||
if (node.declarations[0].id.type !== 'Identifier') return;
|
||||
const name = node.declarations[0].id.name;
|
||||
if (node.declarations[0].init?.type !== 'CallExpression') return;
|
||||
if (node.declarations[0].init.callee.type !== 'Identifier') return;
|
||||
if (node.declarations[0].init.callee.name !== '_export_sfc') return;
|
||||
if (node.declarations[0].init.arguments.length !== 2) return;
|
||||
if (node.declarations[0].init.arguments[0].type !== 'Identifier') return;
|
||||
const ident = node.declarations[0].init.arguments[0].name;
|
||||
if (!ident.startsWith('_sfc_main')) return;
|
||||
if (node.declarations[0].init.arguments[1].type !== 'ArrayExpression') return;
|
||||
if (node.declarations[0].init.arguments[1].elements.length === 0) return;
|
||||
const __cssModulesIndex = node.declarations[0].init.arguments[1].elements.findIndex((x) => {
|
||||
if (x?.type !== 'ArrayExpression') return false;
|
||||
if (x.elements.length !== 2) return false;
|
||||
if (x.elements[0]?.type !== 'Literal') return false;
|
||||
if (x.elements[0].value !== '__cssModules') return false;
|
||||
if (x.elements[1]?.type !== 'Identifier') return false;
|
||||
return true;
|
||||
});
|
||||
if (!~__cssModulesIndex) return;
|
||||
const cssModuleForestName = ((node.declarations[0].init.arguments[1].elements[__cssModulesIndex] as estree.ArrayExpression).elements[1] as estree.Identifier).name;
|
||||
const cssModuleForestNode = parent.body.find((x) => {
|
||||
if (x.type !== 'VariableDeclaration') return false;
|
||||
if (x.declarations.length !== 1) return false;
|
||||
if (x.declarations[0].id.type !== 'Identifier') return false;
|
||||
if (x.declarations[0].id.name !== cssModuleForestName) return false;
|
||||
if (x.declarations[0].init?.type !== 'ObjectExpression') return false;
|
||||
return true;
|
||||
}) as unknown as estree.VariableDeclaration;
|
||||
const moduleForest = new Map((cssModuleForestNode.declarations[0].init as estree.ObjectExpression).properties.flatMap((property) => {
|
||||
if (property.type !== 'Property') return [];
|
||||
if (property.key.type !== 'Literal') return [];
|
||||
if (property.value.type !== 'Identifier') return [];
|
||||
return [[property.key.value as string, property.value.name as string]];
|
||||
}));
|
||||
const sfcMain = parent.body.find((x) => {
|
||||
if (x.type !== 'VariableDeclaration') return false;
|
||||
if (x.declarations.length !== 1) return false;
|
||||
if (x.declarations[0].id.type !== 'Identifier') return false;
|
||||
if (x.declarations[0].id.name !== ident) return false;
|
||||
return true;
|
||||
}) as unknown as estree.VariableDeclaration;
|
||||
if (sfcMain.declarations[0].init?.type !== 'CallExpression') return;
|
||||
if (sfcMain.declarations[0].init.callee.type !== 'Identifier') return;
|
||||
if (sfcMain.declarations[0].init.callee.name !== 'defineComponent') return;
|
||||
if (sfcMain.declarations[0].init.arguments.length !== 1) return;
|
||||
if (sfcMain.declarations[0].init.arguments[0].type !== 'ObjectExpression') return;
|
||||
const setup = sfcMain.declarations[0].init.arguments[0].properties.find((x) => {
|
||||
if (x.type !== 'Property') return false;
|
||||
if (x.key.type !== 'Identifier') return false;
|
||||
if (x.key.name !== 'setup') return false;
|
||||
return true;
|
||||
}) as unknown as estree.Property;
|
||||
if (setup.value.type !== 'FunctionExpression') return;
|
||||
const render = setup.value.body.body.find((x) => {
|
||||
if (x.type !== 'ReturnStatement') return false;
|
||||
return true;
|
||||
}) as unknown as estree.ReturnStatement;
|
||||
if (render.argument?.type !== 'ArrowFunctionExpression') return;
|
||||
if (render.argument.params.length !== 2) return;
|
||||
const ctx = render.argument.params[0];
|
||||
if (ctx.type !== 'Identifier') return;
|
||||
if (ctx.name !== '_ctx') return;
|
||||
if (render.argument.body.type !== 'BlockStatement') return;
|
||||
for (const [key, value] of moduleForest) {
|
||||
const cssModuleTreeNode = parent.body.find((x) => {
|
||||
if (x.type !== 'VariableDeclaration') return false;
|
||||
if (x.declarations.length !== 1) return false;
|
||||
if (x.declarations[0].id.type !== 'Identifier') return false;
|
||||
if (x.declarations[0].id.name !== value) return false;
|
||||
return true;
|
||||
}) as unknown as estree.VariableDeclaration;
|
||||
if (cssModuleTreeNode.declarations[0].init?.type !== 'ObjectExpression') return;
|
||||
const moduleTree = new Map(cssModuleTreeNode.declarations[0].init.properties.flatMap((property) => {
|
||||
if (property.type !== 'Property') return [];
|
||||
const actualKey = property.key.type === 'Identifier' ? property.key.name : property.key.type === 'Literal' ? property.key.value : null;
|
||||
if (typeof actualKey !== 'string') return [];
|
||||
if (property.value.type === 'Literal') return [[actualKey, property.value.value as string]];
|
||||
if (property.value.type !== 'Identifier') return [];
|
||||
const labelledValue = property.value.name;
|
||||
const actualValue = parent.body.find((x) => {
|
||||
if (x.type !== 'VariableDeclaration') return false;
|
||||
if (x.declarations.length !== 1) return false;
|
||||
if (x.declarations[0].id.type !== 'Identifier') return false;
|
||||
if (x.declarations[0].id.name !== labelledValue) return false;
|
||||
return true;
|
||||
}) as unknown as estree.VariableDeclaration;
|
||||
if (actualValue.declarations[0].init?.type !== 'Literal') return [];
|
||||
return [[actualKey, actualValue.declarations[0].init.value as string]];
|
||||
}));
|
||||
(walk as typeof estreeWalker.walk)(render.argument.body, {
|
||||
enter(childNode) {
|
||||
if (childNode.type !== 'MemberExpression') return;
|
||||
if (childNode.object.type !== 'MemberExpression') return;
|
||||
if (childNode.object.object.type !== 'Identifier') return;
|
||||
if (childNode.object.object.name !== ctx.name) return;
|
||||
if (childNode.object.property.type !== 'Identifier') return;
|
||||
if (childNode.object.property.name !== key) return;
|
||||
if (childNode.property.type !== 'Identifier') return;
|
||||
const actualValue = moduleTree.get(childNode.property.name);
|
||||
if (actualValue === undefined) return;
|
||||
this.replace({
|
||||
type: 'Literal',
|
||||
value: actualValue,
|
||||
});
|
||||
},
|
||||
});
|
||||
(walk as typeof estreeWalker.walk)(render.argument.body, {
|
||||
enter(childNode) {
|
||||
if (childNode.type !== 'MemberExpression') return;
|
||||
if (childNode.object.type !== 'MemberExpression') return;
|
||||
if (childNode.object.object.type !== 'Identifier') return;
|
||||
if (childNode.object.object.name !== ctx.name) return;
|
||||
if (childNode.object.property.type !== 'Identifier') return;
|
||||
if (childNode.object.property.name !== key) return;
|
||||
if (childNode.property.type !== 'Identifier') return;
|
||||
console.error(`Undefined style detected: ${key}.${childNode.property.name} (in ${name})`);
|
||||
this.replace({
|
||||
type: 'Identifier',
|
||||
name: 'undefined',
|
||||
});
|
||||
},
|
||||
});
|
||||
(walk as typeof estreeWalker.walk)(render.argument.body, {
|
||||
enter(childNode) {
|
||||
if (childNode.type !== 'CallExpression') return;
|
||||
if (childNode.callee.type !== 'Identifier') return;
|
||||
if (childNode.callee.name !== 'normalizeClass') return;
|
||||
if (childNode.arguments.length !== 1) return;
|
||||
const normalized = normalizeClass(childNode.arguments[0]);
|
||||
if (normalized === null) return;
|
||||
this.replace({
|
||||
type: 'Literal',
|
||||
value: normalized,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
if (node.declarations[0].init.arguments[1].elements.length === 1) {
|
||||
this.replace({
|
||||
type: 'VariableDeclaration',
|
||||
declarations: [{
|
||||
type: 'VariableDeclarator',
|
||||
id: {
|
||||
type: 'Identifier',
|
||||
name: node.declarations[0].id.name,
|
||||
},
|
||||
init: {
|
||||
type: 'Identifier',
|
||||
name: ident,
|
||||
},
|
||||
}],
|
||||
kind: 'const',
|
||||
});
|
||||
} else {
|
||||
this.replace({
|
||||
type: 'VariableDeclaration',
|
||||
declarations: [{
|
||||
type: 'VariableDeclarator',
|
||||
id: {
|
||||
type: 'Identifier',
|
||||
name: node.declarations[0].id.name,
|
||||
},
|
||||
init: {
|
||||
type: 'CallExpression',
|
||||
callee: {
|
||||
type: 'Identifier',
|
||||
name: '_export_sfc',
|
||||
},
|
||||
arguments: [{
|
||||
type: 'Identifier',
|
||||
name: ident,
|
||||
}, {
|
||||
type: 'ArrayExpression',
|
||||
elements: node.declarations[0].init.arguments[1].elements.slice(0, __cssModulesIndex).concat(node.declarations[0].init.arguments[1].elements.slice(__cssModulesIndex + 1)),
|
||||
}],
|
||||
},
|
||||
}],
|
||||
kind: 'const',
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function pluginUnwindCssModuleClassName(): Plugin {
|
||||
return {
|
||||
name: 'UnwindCssModuleClassName',
|
||||
renderChunk(code): { code: string } {
|
||||
const ast = this.parse(code) as unknown as estree.Node;
|
||||
unwindCssModuleClassName(ast);
|
||||
return { code: generate(ast) };
|
||||
},
|
||||
};
|
||||
}
|
@@ -19,26 +19,28 @@
|
||||
"@rollup/plugin-json": "6.0.0",
|
||||
"@rollup/plugin-replace": "5.0.2",
|
||||
"@rollup/pluginutils": "5.0.2",
|
||||
"@syuilo/aiscript": "0.13.2",
|
||||
"@tabler/icons-webfont": "2.17.0",
|
||||
"@vitejs/plugin-vue": "4.2.2",
|
||||
"@vue-macros/reactivity-transform": "0.3.6",
|
||||
"@vue/compiler-sfc": "3.3.1",
|
||||
"autosize": "5.0.2",
|
||||
"blurhash": "2.0.5",
|
||||
"broadcast-channel": "4.20.2",
|
||||
"@syuilo/aiscript": "0.13.3",
|
||||
"@tabler/icons-webfont": "2.21.0",
|
||||
"@vitejs/plugin-vue": "4.2.3",
|
||||
"@vue-macros/reactivity-transform": "0.3.9",
|
||||
"@vue/compiler-sfc": "3.3.4",
|
||||
"astring": "1.8.6",
|
||||
"autosize": "6.0.1",
|
||||
"broadcast-channel": "5.1.0",
|
||||
"browser-image-resizer": "github:misskey-dev/browser-image-resizer#v2.2.1-misskey.3",
|
||||
"buraha": "github:misskey-dev/buraha",
|
||||
"canvas-confetti": "1.6.0",
|
||||
"chart.js": "4.3.0",
|
||||
"chartjs-adapter-date-fns": "3.0.0",
|
||||
"chartjs-chart-matrix": "2.0.1",
|
||||
"chartjs-plugin-gradient": "0.6.1",
|
||||
"chartjs-plugin-zoom": "2.0.1",
|
||||
"chromatic": "6.17.4",
|
||||
"chromatic": "6.18.0",
|
||||
"compare-versions": "5.0.3",
|
||||
"cropperjs": "2.0.0-beta.2",
|
||||
"date-fns": "2.30.0",
|
||||
"escape-regexp": "0.0.1",
|
||||
"estree-walker": "^3.0.3",
|
||||
"eventemitter3": "5.0.1",
|
||||
"gsap": "3.11.5",
|
||||
"idb-keyval": "6.2.1",
|
||||
@@ -53,7 +55,7 @@
|
||||
"punycode": "2.3.0",
|
||||
"querystring": "0.2.1",
|
||||
"rndstr": "1.0.0",
|
||||
"rollup": "3.21.6",
|
||||
"rollup": "3.23.0",
|
||||
"s-age": "1.1.2",
|
||||
"sanitize-html": "2.10.0",
|
||||
"sass": "1.62.1",
|
||||
@@ -61,71 +63,70 @@
|
||||
"strict-event-emitter-types": "2.0.0",
|
||||
"syuilo-password-strength": "0.0.1",
|
||||
"textarea-caret": "3.1.0",
|
||||
"three": "0.151.3",
|
||||
"three": "0.153.0",
|
||||
"throttle-debounce": "5.0.0",
|
||||
"tinycolor2": "1.6.0",
|
||||
"tsc-alias": "1.8.6",
|
||||
"tsconfig-paths": "4.2.0",
|
||||
"twemoji-parser": "14.0.0",
|
||||
"typescript": "5.0.4",
|
||||
"typescript": "5.1.3",
|
||||
"uuid": "9.0.0",
|
||||
"vanilla-tilt": "1.8.0",
|
||||
"vite": "4.3.5",
|
||||
"vue": "3.3.1",
|
||||
"vue-plyr": "7.0.0",
|
||||
"vite": "4.3.9",
|
||||
"vue": "3.3.4",
|
||||
"vue-prism-editor": "2.0.0-alpha.2",
|
||||
"vuedraggable": "next"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@storybook/addon-actions": "7.0.10",
|
||||
"@storybook/addon-essentials": "7.0.10",
|
||||
"@storybook/addon-interactions": "7.0.10",
|
||||
"@storybook/addon-links": "7.0.10",
|
||||
"@storybook/addon-storysource": "7.0.10",
|
||||
"@storybook/addons": "7.0.10",
|
||||
"@storybook/blocks": "7.0.10",
|
||||
"@storybook/core-events": "7.0.10",
|
||||
"@storybook/addon-actions": "7.0.18",
|
||||
"@storybook/addon-essentials": "7.0.18",
|
||||
"@storybook/addon-interactions": "7.0.18",
|
||||
"@storybook/addon-links": "7.0.18",
|
||||
"@storybook/addon-storysource": "7.0.18",
|
||||
"@storybook/addons": "7.0.18",
|
||||
"@storybook/blocks": "7.0.18",
|
||||
"@storybook/core-events": "7.0.18",
|
||||
"@storybook/jest": "0.1.0",
|
||||
"@storybook/manager-api": "7.0.10",
|
||||
"@storybook/preview-api": "7.0.10",
|
||||
"@storybook/react": "7.0.10",
|
||||
"@storybook/react-vite": "7.0.10",
|
||||
"@storybook/manager-api": "7.0.18",
|
||||
"@storybook/preview-api": "7.0.18",
|
||||
"@storybook/react": "7.0.18",
|
||||
"@storybook/react-vite": "7.0.18",
|
||||
"@storybook/testing-library": "0.1.0",
|
||||
"@storybook/theming": "7.0.10",
|
||||
"@storybook/types": "7.0.10",
|
||||
"@storybook/vue3": "7.0.10",
|
||||
"@storybook/vue3-vite": "7.0.10",
|
||||
"@storybook/theming": "7.0.18",
|
||||
"@storybook/types": "7.0.18",
|
||||
"@storybook/vue3": "7.0.18",
|
||||
"@storybook/vue3-vite": "7.0.18",
|
||||
"@testing-library/jest-dom": "5.16.5",
|
||||
"@testing-library/vue": "7.0.0",
|
||||
"@types/escape-regexp": "0.0.1",
|
||||
"@types/estree": "1.0.1",
|
||||
"@types/gulp": "4.0.10",
|
||||
"@types/gulp-rename": "2.0.2",
|
||||
"@types/matter-js": "0.18.3",
|
||||
"@types/matter-js": "0.18.5",
|
||||
"@types/micromatch": "4.0.2",
|
||||
"@types/node": "20.1.3",
|
||||
"@types/node": "20.2.5",
|
||||
"@types/punycode": "2.1.0",
|
||||
"@types/sanitize-html": "2.9.0",
|
||||
"@types/seedrandom": "3.0.5",
|
||||
"@types/testing-library__jest-dom": "^5.14.5",
|
||||
"@types/testing-library__jest-dom": "^5.14.6",
|
||||
"@types/throttle-debounce": "5.0.0",
|
||||
"@types/tinycolor2": "1.4.3",
|
||||
"@types/uuid": "9.0.1",
|
||||
"@types/websocket": "1.0.5",
|
||||
"@types/ws": "8.5.4",
|
||||
"@typescript-eslint/eslint-plugin": "5.59.5",
|
||||
"@typescript-eslint/parser": "5.59.5",
|
||||
"@vitest/coverage-c8": "0.31.0",
|
||||
"@vue/runtime-core": "3.3.1",
|
||||
"astring": "1.8.4",
|
||||
"@typescript-eslint/eslint-plugin": "5.59.8",
|
||||
"@typescript-eslint/parser": "5.59.8",
|
||||
"@vitest/coverage-c8": "0.31.4",
|
||||
"@vue/runtime-core": "3.3.4",
|
||||
"acorn": "^8.8.2",
|
||||
"chokidar-cli": "3.0.0",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "12.12.0",
|
||||
"eslint": "8.40.0",
|
||||
"cypress": "12.13.0",
|
||||
"eslint": "8.41.0",
|
||||
"eslint-plugin-import": "2.27.5",
|
||||
"eslint-plugin-vue": "9.12.0",
|
||||
"eslint-plugin-vue": "9.14.1",
|
||||
"fast-glob": "3.2.12",
|
||||
"happy-dom": "9.16.0",
|
||||
"happy-dom": "9.20.3",
|
||||
"micromatch": "3.1.10",
|
||||
"msw": "1.2.1",
|
||||
"msw-storybook-addon": "1.8.0",
|
||||
@@ -133,13 +134,13 @@
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"start-server-and-test": "2.0.0",
|
||||
"storybook": "7.0.10",
|
||||
"storybook": "7.0.18",
|
||||
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
|
||||
"summaly": "github:misskey-dev/summaly",
|
||||
"vite-plugin-turbosnap": "1.0.2",
|
||||
"vitest": "0.31.0",
|
||||
"vitest": "0.31.4",
|
||||
"vitest-fetch-mock": "0.2.2",
|
||||
"vue-eslint-parser": "9.2.1",
|
||||
"vue-tsc": "1.6.4"
|
||||
"vue-eslint-parser": "9.3.0",
|
||||
"vue-tsc": "1.6.5"
|
||||
}
|
||||
}
|
||||
|
14
packages/frontend/src/_boot_.ts
Normal file
14
packages/frontend/src/_boot_.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
// https://vitejs.dev/config/build-options.html#build-modulepreload
|
||||
import 'vite/modulepreload-polyfill';
|
||||
|
||||
import '@/style.scss';
|
||||
import { mainBoot } from './boot/main-boot';
|
||||
import { subBoot } from './boot/sub-boot';
|
||||
|
||||
const subBootPaths = ['/share', '/auth', '/miauth', '/signup-complete'];
|
||||
|
||||
if (subBootPaths.some(i => location.pathname === i || location.pathname.startsWith(i + '/'))) {
|
||||
subBoot();
|
||||
} else {
|
||||
mainBoot();
|
||||
}
|
@@ -3,11 +3,11 @@ import * as misskey from 'misskey-js';
|
||||
import { showSuspendedDialog } from './scripts/show-suspended-dialog';
|
||||
import { i18n } from './i18n';
|
||||
import { miLocalStorage } from './local-storage';
|
||||
import { MenuButton } from './types/menu';
|
||||
import { del, get, set } from '@/scripts/idb-proxy';
|
||||
import { apiUrl } from '@/config';
|
||||
import { waiting, api, popup, popupMenu, success, alert } from '@/os';
|
||||
import { unisonReload, reloadChannel } from '@/scripts/unison-reload';
|
||||
import { MenuButton } from './types/menu';
|
||||
|
||||
// TODO: 他のタブと永続化されたstateを同期
|
||||
|
||||
@@ -101,57 +101,57 @@ function fetchAccount(token: string, id?: string, forceShowDialog?: boolean): Pr
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
.then(res => new Promise<Account | { error: Record<string, any> }>((done2, fail2) => {
|
||||
if (res.status >= 500 && res.status < 600) {
|
||||
.then(res => new Promise<Account | { error: Record<string, any> }>((done2, fail2) => {
|
||||
if (res.status >= 500 && res.status < 600) {
|
||||
// サーバーエラー(5xx)の場合をrejectとする
|
||||
// (認証エラーなど4xxはresolve)
|
||||
return fail2(res);
|
||||
}
|
||||
res.json().then(done2, fail2);
|
||||
}))
|
||||
.then(async res => {
|
||||
if (res.error) {
|
||||
if (res.error.id === 'a8c724b3-6e9c-4b46-b1a8-bc3ed6258370') {
|
||||
return fail2(res);
|
||||
}
|
||||
res.json().then(done2, fail2);
|
||||
}))
|
||||
.then(async res => {
|
||||
if (res.error) {
|
||||
if (res.error.id === 'a8c724b3-6e9c-4b46-b1a8-bc3ed6258370') {
|
||||
// SUSPENDED
|
||||
if (forceShowDialog || $i && (token === $i.token || id === $i.id)) {
|
||||
await showSuspendedDialog();
|
||||
}
|
||||
} else if (res.error.id === 'e5b3b9f0-2b8f-4b9f-9c1f-8c5c1b2e1b1a') {
|
||||
if (forceShowDialog || $i && (token === $i.token || id === $i.id)) {
|
||||
await showSuspendedDialog();
|
||||
}
|
||||
} else if (res.error.id === 'e5b3b9f0-2b8f-4b9f-9c1f-8c5c1b2e1b1a') {
|
||||
// USER_IS_DELETED
|
||||
// アカウントが削除されている
|
||||
if (forceShowDialog || $i && (token === $i.token || id === $i.id)) {
|
||||
await alert({
|
||||
type: 'error',
|
||||
title: i18n.ts.accountDeleted,
|
||||
text: i18n.ts.accountDeletedDescription,
|
||||
});
|
||||
}
|
||||
} else if (res.error.id === 'b0a7f5f8-dc2f-4171-b91f-de88ad238e14') {
|
||||
if (forceShowDialog || $i && (token === $i.token || id === $i.id)) {
|
||||
await alert({
|
||||
type: 'error',
|
||||
title: i18n.ts.accountDeleted,
|
||||
text: i18n.ts.accountDeletedDescription,
|
||||
});
|
||||
}
|
||||
} else if (res.error.id === 'b0a7f5f8-dc2f-4171-b91f-de88ad238e14') {
|
||||
// AUTHENTICATION_FAILED
|
||||
// トークンが無効化されていたりアカウントが削除されたりしている
|
||||
if (forceShowDialog || $i && (token === $i.token || id === $i.id)) {
|
||||
if (forceShowDialog || $i && (token === $i.token || id === $i.id)) {
|
||||
await alert({
|
||||
type: 'error',
|
||||
title: i18n.ts.tokenRevoked,
|
||||
text: i18n.ts.tokenRevokedDescription,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
await alert({
|
||||
type: 'error',
|
||||
title: i18n.ts.tokenRevoked,
|
||||
text: i18n.ts.tokenRevokedDescription,
|
||||
title: i18n.ts.failedToFetchAccountInformation,
|
||||
text: JSON.stringify(res.error),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
await alert({
|
||||
type: 'error',
|
||||
title: i18n.ts.failedToFetchAccountInformation,
|
||||
text: JSON.stringify(res.error),
|
||||
});
|
||||
}
|
||||
|
||||
// rejectかつ理由がtrueの場合、削除対象であることを示す
|
||||
fail(true);
|
||||
} else {
|
||||
(res as Account).token = token;
|
||||
done(res as Account);
|
||||
}
|
||||
})
|
||||
.catch(fail);
|
||||
// rejectかつ理由がtrueの場合、削除対象であることを示す
|
||||
fail(true);
|
||||
} else {
|
||||
(res as Account).token = token;
|
||||
done(res as Account);
|
||||
}
|
||||
})
|
||||
.catch(fail);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -305,3 +305,7 @@ export async function openAccountMenu(opts: {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (_DEV_) {
|
||||
(window as any).$i = $i;
|
||||
}
|
||||
|
262
packages/frontend/src/boot/common.ts
Normal file
262
packages/frontend/src/boot/common.ts
Normal file
@@ -0,0 +1,262 @@
|
||||
import { computed, createApp, watch, markRaw, version as vueVersion, defineAsyncComponent, App } from 'vue';
|
||||
import { compareVersions } from 'compare-versions';
|
||||
import widgets from '@/widgets';
|
||||
import directives from '@/directives';
|
||||
import components from '@/components';
|
||||
import { version, ui, lang, updateLocale } from '@/config';
|
||||
import { applyTheme } from '@/scripts/theme';
|
||||
import { isDeviceDarkmode } from '@/scripts/is-device-darkmode';
|
||||
import { i18n, updateI18n } from '@/i18n';
|
||||
import { confirm, alert, post, popup, toast } from '@/os';
|
||||
import { $i, refreshAccount, login, updateAccount, signout } from '@/account';
|
||||
import { defaultStore, ColdDeviceStorage } from '@/store';
|
||||
import { fetchInstance, instance } from '@/instance';
|
||||
import { deviceKind } from '@/scripts/device-kind';
|
||||
import { reloadChannel } from '@/scripts/unison-reload';
|
||||
import { reactionPicker } from '@/scripts/reaction-picker';
|
||||
import { getUrlWithoutLoginId } from '@/scripts/login-id';
|
||||
import { getAccountFromId } from '@/scripts/get-account-from-id';
|
||||
import { deckStore } from '@/ui/deck/deck-store';
|
||||
import { miLocalStorage } from '@/local-storage';
|
||||
import { fetchCustomEmojis } from '@/custom-emojis';
|
||||
import { mainRouter } from '@/router';
|
||||
|
||||
export async function common(createVue: () => App<Element>) {
|
||||
console.info(`Misskey v${version}`);
|
||||
|
||||
if (_DEV_) {
|
||||
console.warn('Development mode!!!');
|
||||
|
||||
console.info(`vue ${vueVersion}`);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(window as any).$i = $i;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(window as any).$store = defaultStore;
|
||||
|
||||
window.addEventListener('error', event => {
|
||||
console.error(event);
|
||||
/*
|
||||
alert({
|
||||
type: 'error',
|
||||
title: 'DEV: Unhandled error',
|
||||
text: event.message
|
||||
});
|
||||
*/
|
||||
});
|
||||
|
||||
window.addEventListener('unhandledrejection', event => {
|
||||
console.error(event);
|
||||
/*
|
||||
alert({
|
||||
type: 'error',
|
||||
title: 'DEV: Unhandled promise rejection',
|
||||
text: event.reason
|
||||
});
|
||||
*/
|
||||
});
|
||||
}
|
||||
|
||||
const splash = document.getElementById('splash');
|
||||
// 念のためnullチェック(HTMLが古い場合があるため(そのうち消す))
|
||||
if (splash) splash.addEventListener('transitionend', () => {
|
||||
splash.remove();
|
||||
});
|
||||
|
||||
let isClientUpdated = false;
|
||||
|
||||
//#region クライアントが更新されたかチェック
|
||||
const lastVersion = miLocalStorage.getItem('lastVersion');
|
||||
if (lastVersion !== version) {
|
||||
miLocalStorage.setItem('lastVersion', version);
|
||||
|
||||
// テーマリビルドするため
|
||||
miLocalStorage.removeItem('theme');
|
||||
|
||||
try { // 変なバージョン文字列来るとcompareVersionsでエラーになるため
|
||||
if (lastVersion != null && compareVersions(version, lastVersion) === 1) {
|
||||
isClientUpdated = true;
|
||||
}
|
||||
} catch (err) { /* empty */ }
|
||||
}
|
||||
//#endregion
|
||||
|
||||
//#region Detect language & fetch translations
|
||||
const localeVersion = miLocalStorage.getItem('localeVersion');
|
||||
const localeOutdated = (localeVersion == null || localeVersion !== version);
|
||||
if (localeOutdated) {
|
||||
const res = await window.fetch(`/assets/locales/${lang}.${version}.json`);
|
||||
if (res.status === 200) {
|
||||
const newLocale = await res.text();
|
||||
const parsedNewLocale = JSON.parse(newLocale);
|
||||
miLocalStorage.setItem('locale', newLocale);
|
||||
miLocalStorage.setItem('localeVersion', version);
|
||||
updateLocale(parsedNewLocale);
|
||||
updateI18n(parsedNewLocale);
|
||||
}
|
||||
}
|
||||
//#endregion
|
||||
|
||||
// タッチデバイスでCSSの:hoverを機能させる
|
||||
document.addEventListener('touchend', () => {}, { passive: true });
|
||||
|
||||
// 一斉リロード
|
||||
reloadChannel.addEventListener('message', path => {
|
||||
if (path !== null) location.href = path;
|
||||
else location.reload();
|
||||
});
|
||||
|
||||
// If mobile, insert the viewport meta tag
|
||||
if (['smartphone', 'tablet'].includes(deviceKind)) {
|
||||
const viewport = document.getElementsByName('viewport').item(0);
|
||||
viewport.setAttribute('content',
|
||||
`${viewport.getAttribute('content')}, minimum-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover`);
|
||||
}
|
||||
|
||||
//#region Set lang attr
|
||||
const html = document.documentElement;
|
||||
html.setAttribute('lang', lang);
|
||||
//#endregion
|
||||
|
||||
await defaultStore.ready;
|
||||
await deckStore.ready;
|
||||
|
||||
const fetchInstanceMetaPromise = fetchInstance();
|
||||
|
||||
fetchInstanceMetaPromise.then(() => {
|
||||
miLocalStorage.setItem('v', instance.version);
|
||||
});
|
||||
|
||||
//#region loginId
|
||||
const params = new URLSearchParams(location.search);
|
||||
const loginId = params.get('loginId');
|
||||
|
||||
if (loginId) {
|
||||
const target = getUrlWithoutLoginId(location.href);
|
||||
|
||||
if (!$i || $i.id !== loginId) {
|
||||
const account = await getAccountFromId(loginId);
|
||||
if (account) {
|
||||
await login(account.token, target);
|
||||
}
|
||||
}
|
||||
|
||||
history.replaceState({ misskey: 'loginId' }, '', target);
|
||||
}
|
||||
//#endregion
|
||||
|
||||
// NOTE: この処理は必ずクライアント更新チェック処理より後に来ること(テーマ再構築のため)
|
||||
watch(defaultStore.reactiveState.darkMode, (darkMode) => {
|
||||
applyTheme(darkMode ? ColdDeviceStorage.get('darkTheme') : ColdDeviceStorage.get('lightTheme'));
|
||||
}, { immediate: miLocalStorage.getItem('theme') == null });
|
||||
|
||||
const darkTheme = computed(ColdDeviceStorage.makeGetterSetter('darkTheme'));
|
||||
const lightTheme = computed(ColdDeviceStorage.makeGetterSetter('lightTheme'));
|
||||
|
||||
watch(darkTheme, (theme) => {
|
||||
if (defaultStore.state.darkMode) {
|
||||
applyTheme(theme);
|
||||
}
|
||||
});
|
||||
|
||||
watch(lightTheme, (theme) => {
|
||||
if (!defaultStore.state.darkMode) {
|
||||
applyTheme(theme);
|
||||
}
|
||||
});
|
||||
|
||||
//#region Sync dark mode
|
||||
if (ColdDeviceStorage.get('syncDeviceDarkMode')) {
|
||||
defaultStore.set('darkMode', isDeviceDarkmode());
|
||||
}
|
||||
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addListener(mql => {
|
||||
if (ColdDeviceStorage.get('syncDeviceDarkMode')) {
|
||||
defaultStore.set('darkMode', mql.matches);
|
||||
}
|
||||
});
|
||||
//#endregion
|
||||
|
||||
fetchInstanceMetaPromise.then(() => {
|
||||
if (defaultStore.state.themeInitial) {
|
||||
if (instance.defaultLightTheme != null) ColdDeviceStorage.set('lightTheme', JSON.parse(instance.defaultLightTheme));
|
||||
if (instance.defaultDarkTheme != null) ColdDeviceStorage.set('darkTheme', JSON.parse(instance.defaultDarkTheme));
|
||||
defaultStore.set('themeInitial', false);
|
||||
}
|
||||
});
|
||||
|
||||
watch(defaultStore.reactiveState.useBlurEffectForModal, v => {
|
||||
document.documentElement.style.setProperty('--modalBgFilter', v ? 'blur(4px)' : 'none');
|
||||
}, { immediate: true });
|
||||
|
||||
watch(defaultStore.reactiveState.useBlurEffect, v => {
|
||||
if (v) {
|
||||
document.documentElement.style.removeProperty('--blur');
|
||||
} else {
|
||||
document.documentElement.style.setProperty('--blur', 'none');
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
//#region Fetch user
|
||||
if ($i && $i.token) {
|
||||
if (_DEV_) {
|
||||
console.log('account cache found. refreshing...');
|
||||
}
|
||||
|
||||
refreshAccount();
|
||||
}
|
||||
//#endregion
|
||||
|
||||
try {
|
||||
await fetchCustomEmojis();
|
||||
} catch (err) { /* empty */ }
|
||||
|
||||
const app = createVue();
|
||||
|
||||
if (_DEV_) {
|
||||
app.config.performance = true;
|
||||
}
|
||||
|
||||
widgets(app);
|
||||
directives(app);
|
||||
components(app);
|
||||
|
||||
// https://github.com/misskey-dev/misskey/pull/8575#issuecomment-1114239210
|
||||
// なぜか2回実行されることがあるため、mountするdivを1つに制限する
|
||||
const rootEl = ((): HTMLElement => {
|
||||
const MISSKEY_MOUNT_DIV_ID = 'misskey_app';
|
||||
|
||||
const currentRoot = document.getElementById(MISSKEY_MOUNT_DIV_ID);
|
||||
|
||||
if (currentRoot) {
|
||||
console.warn('multiple import detected');
|
||||
return currentRoot;
|
||||
}
|
||||
|
||||
const root = document.createElement('div');
|
||||
root.id = MISSKEY_MOUNT_DIV_ID;
|
||||
document.body.appendChild(root);
|
||||
return root;
|
||||
})();
|
||||
|
||||
app.mount(rootEl);
|
||||
|
||||
// boot.jsのやつを解除
|
||||
window.onerror = null;
|
||||
window.onunhandledrejection = null;
|
||||
|
||||
removeSplash();
|
||||
|
||||
return {
|
||||
isClientUpdated,
|
||||
app,
|
||||
};
|
||||
}
|
||||
|
||||
function removeSplash() {
|
||||
const splash = document.getElementById('splash');
|
||||
if (splash) {
|
||||
splash.style.opacity = '0';
|
||||
splash.style.pointerEvents = 'none';
|
||||
}
|
||||
}
|
254
packages/frontend/src/boot/main-boot.ts
Normal file
254
packages/frontend/src/boot/main-boot.ts
Normal file
@@ -0,0 +1,254 @@
|
||||
import { computed, createApp, watch, markRaw, version as vueVersion, defineAsyncComponent } from 'vue';
|
||||
import { common } from './common';
|
||||
import { version, ui, lang, updateLocale } from '@/config';
|
||||
import { i18n, updateI18n } from '@/i18n';
|
||||
import { confirm, alert, post, popup, toast } from '@/os';
|
||||
import { useStream } from '@/stream';
|
||||
import * as sound from '@/scripts/sound';
|
||||
import { $i, refreshAccount, login, updateAccount, signout } from '@/account';
|
||||
import { defaultStore, ColdDeviceStorage } from '@/store';
|
||||
import { makeHotkey } from '@/scripts/hotkey';
|
||||
import { reactionPicker } from '@/scripts/reaction-picker';
|
||||
import { miLocalStorage } from '@/local-storage';
|
||||
import { claimAchievement, claimedAchievements } from '@/scripts/achievements';
|
||||
import { mainRouter } from '@/router';
|
||||
import { initializeSw } from '@/scripts/initialize-sw';
|
||||
|
||||
export async function mainBoot() {
|
||||
const { isClientUpdated } = await common(() => createApp(
|
||||
new URLSearchParams(window.location.search).has('zen') || (ui === 'deck' && location.pathname !== '/') ? defineAsyncComponent(() => import('@/ui/zen.vue')) :
|
||||
!$i ? defineAsyncComponent(() => import('@/ui/visitor.vue')) :
|
||||
ui === 'deck' ? defineAsyncComponent(() => import('@/ui/deck.vue')) :
|
||||
ui === 'classic' ? defineAsyncComponent(() => import('@/ui/classic.vue')) :
|
||||
defineAsyncComponent(() => import('@/ui/universal.vue')),
|
||||
));
|
||||
|
||||
reactionPicker.init();
|
||||
|
||||
if (isClientUpdated && $i) {
|
||||
popup(defineAsyncComponent(() => import('@/components/MkUpdated.vue')), {}, {}, 'closed');
|
||||
}
|
||||
|
||||
const stream = useStream();
|
||||
|
||||
let reloadDialogShowing = false;
|
||||
stream.on('_disconnected_', async () => {
|
||||
if (defaultStore.state.serverDisconnectedBehavior === 'reload') {
|
||||
location.reload();
|
||||
} else if (defaultStore.state.serverDisconnectedBehavior === 'dialog') {
|
||||
if (reloadDialogShowing) return;
|
||||
reloadDialogShowing = true;
|
||||
const { canceled } = await confirm({
|
||||
type: 'warning',
|
||||
title: i18n.ts.disconnectedFromServer,
|
||||
text: i18n.ts.reloadConfirm,
|
||||
});
|
||||
reloadDialogShowing = false;
|
||||
if (!canceled) {
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
for (const plugin of ColdDeviceStorage.get('plugins').filter(p => p.active)) {
|
||||
import('../plugin').then(async ({ install }) => {
|
||||
// Workaround for https://bugs.webkit.org/show_bug.cgi?id=242740
|
||||
await new Promise(r => setTimeout(r, 0));
|
||||
install(plugin);
|
||||
});
|
||||
}
|
||||
|
||||
const hotkeys = {
|
||||
'd': (): void => {
|
||||
defaultStore.set('darkMode', !defaultStore.state.darkMode);
|
||||
},
|
||||
's': (): void => {
|
||||
mainRouter.push('/search');
|
||||
},
|
||||
};
|
||||
|
||||
if ($i) {
|
||||
// only add post shortcuts if logged in
|
||||
hotkeys['p|n'] = post;
|
||||
|
||||
defaultStore.loaded.then(() => {
|
||||
if (defaultStore.state.accountSetupWizard !== -1) {
|
||||
popup(defineAsyncComponent(() => import('@/components/MkUserSetupDialog.vue')), {}, {}, 'closed');
|
||||
}
|
||||
});
|
||||
|
||||
if ($i.isDeleted) {
|
||||
alert({
|
||||
type: 'warning',
|
||||
text: i18n.ts.accountDeletionInProgress,
|
||||
});
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const m = now.getMonth() + 1;
|
||||
const d = now.getDate();
|
||||
|
||||
if ($i.birthday) {
|
||||
const bm = parseInt($i.birthday.split('-')[1]);
|
||||
const bd = parseInt($i.birthday.split('-')[2]);
|
||||
if (m === bm && d === bd) {
|
||||
claimAchievement('loggedInOnBirthday');
|
||||
}
|
||||
}
|
||||
|
||||
if (m === 1 && d === 1) {
|
||||
claimAchievement('loggedInOnNewYearsDay');
|
||||
}
|
||||
|
||||
if ($i.loggedInDays >= 3) claimAchievement('login3');
|
||||
if ($i.loggedInDays >= 7) claimAchievement('login7');
|
||||
if ($i.loggedInDays >= 15) claimAchievement('login15');
|
||||
if ($i.loggedInDays >= 30) claimAchievement('login30');
|
||||
if ($i.loggedInDays >= 60) claimAchievement('login60');
|
||||
if ($i.loggedInDays >= 100) claimAchievement('login100');
|
||||
if ($i.loggedInDays >= 200) claimAchievement('login200');
|
||||
if ($i.loggedInDays >= 300) claimAchievement('login300');
|
||||
if ($i.loggedInDays >= 400) claimAchievement('login400');
|
||||
if ($i.loggedInDays >= 500) claimAchievement('login500');
|
||||
if ($i.loggedInDays >= 600) claimAchievement('login600');
|
||||
if ($i.loggedInDays >= 700) claimAchievement('login700');
|
||||
if ($i.loggedInDays >= 800) claimAchievement('login800');
|
||||
if ($i.loggedInDays >= 900) claimAchievement('login900');
|
||||
if ($i.loggedInDays >= 1000) claimAchievement('login1000');
|
||||
|
||||
if ($i.notesCount > 0) claimAchievement('notes1');
|
||||
if ($i.notesCount >= 10) claimAchievement('notes10');
|
||||
if ($i.notesCount >= 100) claimAchievement('notes100');
|
||||
if ($i.notesCount >= 500) claimAchievement('notes500');
|
||||
if ($i.notesCount >= 1000) claimAchievement('notes1000');
|
||||
if ($i.notesCount >= 5000) claimAchievement('notes5000');
|
||||
if ($i.notesCount >= 10000) claimAchievement('notes10000');
|
||||
if ($i.notesCount >= 20000) claimAchievement('notes20000');
|
||||
if ($i.notesCount >= 30000) claimAchievement('notes30000');
|
||||
if ($i.notesCount >= 40000) claimAchievement('notes40000');
|
||||
if ($i.notesCount >= 50000) claimAchievement('notes50000');
|
||||
if ($i.notesCount >= 60000) claimAchievement('notes60000');
|
||||
if ($i.notesCount >= 70000) claimAchievement('notes70000');
|
||||
if ($i.notesCount >= 80000) claimAchievement('notes80000');
|
||||
if ($i.notesCount >= 90000) claimAchievement('notes90000');
|
||||
if ($i.notesCount >= 100000) claimAchievement('notes100000');
|
||||
|
||||
if ($i.followersCount > 0) claimAchievement('followers1');
|
||||
if ($i.followersCount >= 10) claimAchievement('followers10');
|
||||
if ($i.followersCount >= 50) claimAchievement('followers50');
|
||||
if ($i.followersCount >= 100) claimAchievement('followers100');
|
||||
if ($i.followersCount >= 300) claimAchievement('followers300');
|
||||
if ($i.followersCount >= 500) claimAchievement('followers500');
|
||||
if ($i.followersCount >= 1000) claimAchievement('followers1000');
|
||||
|
||||
if (Date.now() - new Date($i.createdAt).getTime() > 1000 * 60 * 60 * 24 * 365) {
|
||||
claimAchievement('passedSinceAccountCreated1');
|
||||
}
|
||||
if (Date.now() - new Date($i.createdAt).getTime() > 1000 * 60 * 60 * 24 * 365 * 2) {
|
||||
claimAchievement('passedSinceAccountCreated2');
|
||||
}
|
||||
if (Date.now() - new Date($i.createdAt).getTime() > 1000 * 60 * 60 * 24 * 365 * 3) {
|
||||
claimAchievement('passedSinceAccountCreated3');
|
||||
}
|
||||
|
||||
if (claimedAchievements.length >= 30) {
|
||||
claimAchievement('collectAchievements30');
|
||||
}
|
||||
|
||||
window.setInterval(() => {
|
||||
if (Math.floor(Math.random() * 20000) === 0) {
|
||||
claimAchievement('justPlainLucky');
|
||||
}
|
||||
}, 1000 * 10);
|
||||
|
||||
window.setTimeout(() => {
|
||||
claimAchievement('client30min');
|
||||
}, 1000 * 60 * 30);
|
||||
|
||||
window.setTimeout(() => {
|
||||
claimAchievement('client60min');
|
||||
}, 1000 * 60 * 60);
|
||||
|
||||
const lastUsed = miLocalStorage.getItem('lastUsed');
|
||||
if (lastUsed) {
|
||||
const lastUsedDate = parseInt(lastUsed, 10);
|
||||
// 二時間以上前なら
|
||||
if (Date.now() - lastUsedDate > 1000 * 60 * 60 * 2) {
|
||||
toast(i18n.t('welcomeBackWithName', {
|
||||
name: $i.name || $i.username,
|
||||
}));
|
||||
}
|
||||
}
|
||||
miLocalStorage.setItem('lastUsed', Date.now().toString());
|
||||
|
||||
const latestDonationInfoShownAt = miLocalStorage.getItem('latestDonationInfoShownAt');
|
||||
const neverShowDonationInfo = miLocalStorage.getItem('neverShowDonationInfo');
|
||||
if (neverShowDonationInfo !== 'true' && (new Date($i.createdAt).getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 3))) && !location.pathname.startsWith('/miauth')) {
|
||||
if (latestDonationInfoShownAt == null || (new Date(latestDonationInfoShownAt).getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 30)))) {
|
||||
popup(defineAsyncComponent(() => import('@/components/MkDonation.vue')), {}, {}, 'closed');
|
||||
}
|
||||
}
|
||||
|
||||
if ('Notification' in window) {
|
||||
// 許可を得ていなかったらリクエスト
|
||||
if (Notification.permission === 'default') {
|
||||
Notification.requestPermission();
|
||||
}
|
||||
}
|
||||
|
||||
const main = markRaw(stream.useChannel('main', null, 'System'));
|
||||
|
||||
// 自分の情報が更新されたとき
|
||||
main.on('meUpdated', i => {
|
||||
updateAccount(i);
|
||||
});
|
||||
|
||||
main.on('readAllNotifications', () => {
|
||||
updateAccount({ hasUnreadNotification: false });
|
||||
});
|
||||
|
||||
main.on('unreadNotification', () => {
|
||||
updateAccount({ hasUnreadNotification: true });
|
||||
});
|
||||
|
||||
main.on('unreadMention', () => {
|
||||
updateAccount({ hasUnreadMentions: true });
|
||||
});
|
||||
|
||||
main.on('readAllUnreadMentions', () => {
|
||||
updateAccount({ hasUnreadMentions: false });
|
||||
});
|
||||
|
||||
main.on('unreadSpecifiedNote', () => {
|
||||
updateAccount({ hasUnreadSpecifiedNotes: true });
|
||||
});
|
||||
|
||||
main.on('readAllUnreadSpecifiedNotes', () => {
|
||||
updateAccount({ hasUnreadSpecifiedNotes: false });
|
||||
});
|
||||
|
||||
main.on('readAllAntennas', () => {
|
||||
updateAccount({ hasUnreadAntenna: false });
|
||||
});
|
||||
|
||||
main.on('unreadAntenna', () => {
|
||||
updateAccount({ hasUnreadAntenna: true });
|
||||
sound.play('antenna');
|
||||
});
|
||||
|
||||
main.on('readAllAnnouncements', () => {
|
||||
updateAccount({ hasUnreadAnnouncement: false });
|
||||
});
|
||||
|
||||
// トークンが再生成されたとき
|
||||
// このままではMisskeyが利用できないので強制的にサインアウトさせる
|
||||
main.on('myTokenRegenerated', () => {
|
||||
signout();
|
||||
});
|
||||
}
|
||||
|
||||
// shortcut
|
||||
document.addEventListener('keydown', makeHotkey(hotkeys));
|
||||
|
||||
initializeSw();
|
||||
}
|
8
packages/frontend/src/boot/sub-boot.ts
Normal file
8
packages/frontend/src/boot/sub-boot.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { computed, createApp, watch, markRaw, version as vueVersion, defineAsyncComponent } from 'vue';
|
||||
import { common } from './common';
|
||||
|
||||
export async function subBoot() {
|
||||
const { isClientUpdated } = await common(() => createApp(
|
||||
defineAsyncComponent(() => import('@/ui/minimum.vue')),
|
||||
));
|
||||
}
|
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<MkWindow ref="uiWindow" :initial-width="400" :initial-height="500" :can-resize="true" @closed="emit('closed')">
|
||||
<MkWindow ref="uiWindow" :initialWidth="400" :initialHeight="500" :canResize="true" @closed="emit('closed')">
|
||||
<template #header>
|
||||
<i class="ti ti-exclamation-circle" style="margin-right: 0.5em;"></i>
|
||||
<I18n :src="i18n.ts.reportAbuseOf" tag="span">
|
||||
@@ -8,8 +8,8 @@
|
||||
</template>
|
||||
</I18n>
|
||||
</template>
|
||||
<MkSpacer :margin-min="20" :margin-max="28">
|
||||
<div class="dpvffvvy _gaps_m">
|
||||
<MkSpacer :marginMin="20" :marginMax="28">
|
||||
<div class="_gaps_m" :class="$style.root">
|
||||
<div class="">
|
||||
<MkTextarea v-model="comment">
|
||||
<template #label>{{ i18n.ts.details }}</template>
|
||||
@@ -60,8 +60,8 @@ function send() {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dpvffvvy {
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
--root-margin: 16px;
|
||||
}
|
||||
</style>
|
||||
|
@@ -7,11 +7,11 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { UserLite } from 'misskey-js/built/entities';
|
||||
import MkMention from './MkMention.vue';
|
||||
import { i18n } from '@/i18n';
|
||||
import { host as localHost } from '@/config';
|
||||
import { ref } from 'vue';
|
||||
import { UserLite } from 'misskey-js/built/entities';
|
||||
import { api } from '@/os';
|
||||
|
||||
const user = ref<UserLite>();
|
||||
|
@@ -3,7 +3,14 @@
|
||||
<div v-if="achievements" :class="$style.root">
|
||||
<div v-for="achievement in achievements" :key="achievement" :class="$style.achievement" class="_panel">
|
||||
<div :class="$style.icon">
|
||||
<div :class="[$style.iconFrame, $style['iconFrame_' + ACHIEVEMENT_BADGES[achievement.name].frame]]">
|
||||
<div
|
||||
:class="[$style.iconFrame, {
|
||||
[$style.iconFrame_bronze]: ACHIEVEMENT_BADGES[achievement.name].frame === 'bronze',
|
||||
[$style.iconFrame_silver]: ACHIEVEMENT_BADGES[achievement.name].frame === 'silver',
|
||||
[$style.iconFrame_gold]: ACHIEVEMENT_BADGES[achievement.name].frame === 'gold',
|
||||
[$style.iconFrame_platinum]: ACHIEVEMENT_BADGES[achievement.name].frame === 'platinum',
|
||||
}]"
|
||||
>
|
||||
<div :class="[$style.iconInner]" :style="{ background: ACHIEVEMENT_BADGES[achievement.name].bg }">
|
||||
<img :class="$style.iconImg" :src="ACHIEVEMENT_BADGES[achievement.name].img">
|
||||
</div>
|
||||
|
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import isChromatic from 'chromatic/isChromatic';
|
||||
import MkAnalogClock from './MkAnalogClock.vue';
|
||||
import isChromatic from 'chromatic';
|
||||
export const Default = {
|
||||
render(args) {
|
||||
return {
|
||||
|
@@ -39,6 +39,7 @@
|
||||
-->
|
||||
|
||||
<line
|
||||
ref="sLine"
|
||||
:class="[$style.s, { [$style.animate]: !disableSAnimate && sAnimation !== 'none', [$style.elastic]: sAnimation === 'elastic', [$style.easeOut]: sAnimation === 'easeOut' }]"
|
||||
:x1="5 - (0 * (sHandLengthRatio * handsTailLength))"
|
||||
:y1="5 + (1 * (sHandLengthRatio * handsTailLength))"
|
||||
@@ -73,9 +74,10 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, onBeforeUnmount } from 'vue';
|
||||
import { computed, onMounted, onBeforeUnmount, ref } from 'vue';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { globalEvents } from '@/events.js';
|
||||
import { defaultIdlingRenderScheduler } from '@/scripts/idle-render.js';
|
||||
|
||||
// https://stackoverflow.com/questions/1878907/how-can-i-find-the-difference-between-two-angles
|
||||
const angleDiff = (a: number, b: number) => {
|
||||
@@ -145,6 +147,7 @@ let mAngle = $ref<number>(0);
|
||||
let sAngle = $ref<number>(0);
|
||||
let disableSAnimate = $ref(false);
|
||||
let sOneRound = false;
|
||||
const sLine = ref<SVGPathElement>();
|
||||
|
||||
function tick() {
|
||||
const now = props.now();
|
||||
@@ -160,17 +163,21 @@ function tick() {
|
||||
}
|
||||
hAngle = Math.PI * (h % (props.twentyfour ? 24 : 12) + (m + s / 60) / 60) / (props.twentyfour ? 12 : 6);
|
||||
mAngle = Math.PI * (m + s / 60) / 30;
|
||||
if (sOneRound) { // 秒針が一周した際のアニメーションをよしなに処理する(これが無いと秒が59->0になったときに期待したアニメーションにならない)
|
||||
if (sOneRound && sLine.value) { // 秒針が一周した際のアニメーションをよしなに処理する(これが無いと秒が59->0になったときに期待したアニメーションにならない)
|
||||
sAngle = Math.PI * 60 / 30;
|
||||
window.setTimeout(() => {
|
||||
defaultIdlingRenderScheduler.delete(tick);
|
||||
sLine.value.addEventListener('transitionend', () => {
|
||||
disableSAnimate = true;
|
||||
window.setTimeout(() => {
|
||||
requestAnimationFrame(() => {
|
||||
sAngle = 0;
|
||||
window.setTimeout(() => {
|
||||
requestAnimationFrame(() => {
|
||||
disableSAnimate = false;
|
||||
}, 100);
|
||||
}, 100);
|
||||
}, 700);
|
||||
if (enabled) {
|
||||
defaultIdlingRenderScheduler.add(tick);
|
||||
}
|
||||
});
|
||||
});
|
||||
}, { once: true });
|
||||
} else {
|
||||
sAngle = Math.PI * s / 30;
|
||||
}
|
||||
@@ -194,20 +201,13 @@ function calcColors() {
|
||||
calcColors();
|
||||
|
||||
onMounted(() => {
|
||||
const update = () => {
|
||||
if (enabled) {
|
||||
tick();
|
||||
window.setTimeout(update, 1000);
|
||||
}
|
||||
};
|
||||
update();
|
||||
|
||||
defaultIdlingRenderScheduler.add(tick);
|
||||
globalEvents.on('themeChanged', calcColors);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
enabled = false;
|
||||
|
||||
defaultIdlingRenderScheduler.delete(tick);
|
||||
globalEvents.off('themeChanged', calcColors);
|
||||
});
|
||||
</script>
|
||||
|
243
packages/frontend/src/components/MkAnimBg.vue
Normal file
243
packages/frontend/src/components/MkAnimBg.vue
Normal file
@@ -0,0 +1,243 @@
|
||||
<template>
|
||||
<canvas ref="canvasEl" style="width: 100%; height: 100%; pointer-events: none;"></canvas>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted, shallowRef } from 'vue';
|
||||
import isChromatic from 'chromatic/isChromatic';
|
||||
|
||||
const canvasEl = shallowRef<HTMLCanvasElement>();
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
scale?: number;
|
||||
focus?: number;
|
||||
}>(), {
|
||||
scale: 1.0,
|
||||
focus: 1.0,
|
||||
});
|
||||
|
||||
function loadShader(gl, type, source) {
|
||||
const shader = gl.createShader(type);
|
||||
|
||||
gl.shaderSource(shader, source);
|
||||
gl.compileShader(shader);
|
||||
|
||||
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
||||
alert(
|
||||
`falied to compile shader: ${gl.getShaderInfoLog(shader)}`,
|
||||
);
|
||||
gl.deleteShader(shader);
|
||||
return null;
|
||||
}
|
||||
|
||||
return shader;
|
||||
}
|
||||
|
||||
function initShaderProgram(gl, vsSource, fsSource) {
|
||||
const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource);
|
||||
const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource);
|
||||
|
||||
const shaderProgram = gl.createProgram();
|
||||
gl.attachShader(shaderProgram, vertexShader);
|
||||
gl.attachShader(shaderProgram, fragmentShader);
|
||||
gl.linkProgram(shaderProgram);
|
||||
|
||||
if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
|
||||
alert(
|
||||
`failed to init shader: ${gl.getProgramInfoLog(
|
||||
shaderProgram,
|
||||
)}`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
return shaderProgram;
|
||||
}
|
||||
|
||||
let handle: ReturnType<typeof window['requestAnimationFrame']> | null = null;
|
||||
|
||||
onMounted(() => {
|
||||
const canvas = canvasEl.value!;
|
||||
canvas.width = canvas.offsetWidth;
|
||||
canvas.height = canvas.offsetHeight;
|
||||
|
||||
const gl = canvas.getContext('webgl', { premultipliedAlpha: true });
|
||||
if (gl == null) return;
|
||||
|
||||
gl.clearColor(0.0, 0.0, 0.0, 0.0);
|
||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
|
||||
const positionBuffer = gl.createBuffer();
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
|
||||
|
||||
const shaderProgram = initShaderProgram(gl, `
|
||||
attribute vec2 vertex;
|
||||
|
||||
uniform vec2 u_scale;
|
||||
|
||||
varying vec2 v_pos;
|
||||
|
||||
void main() {
|
||||
gl_Position = vec4(vertex, 0.0, 1.0);
|
||||
v_pos = vertex / u_scale;
|
||||
}
|
||||
`, `
|
||||
precision mediump float;
|
||||
|
||||
vec3 mod289(vec3 x) {
|
||||
return x - floor(x * (1.0 / 289.0)) * 289.0;
|
||||
}
|
||||
|
||||
vec2 mod289(vec2 x) {
|
||||
return x - floor(x * (1.0 / 289.0)) * 289.0;
|
||||
}
|
||||
|
||||
vec3 permute(vec3 x) {
|
||||
return mod289(((x*34.0)+1.0)*x);
|
||||
}
|
||||
|
||||
float snoise(vec2 v) {
|
||||
const vec4 C = vec4(0.211324865405187,
|
||||
0.366025403784439,
|
||||
-0.577350269189626,
|
||||
0.024390243902439);
|
||||
|
||||
vec2 i = floor(v + dot(v, C.yy) );
|
||||
vec2 x0 = v - i + dot(i, C.xx);
|
||||
|
||||
vec2 i1;
|
||||
i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
|
||||
vec4 x12 = x0.xyxy + C.xxzz;
|
||||
x12.xy -= i1;
|
||||
|
||||
i = mod289(i);
|
||||
vec3 p = permute( permute( i.y + vec3(0.0, i1.y, 1.0 ))
|
||||
+ i.x + vec3(0.0, i1.x, 1.0 ));
|
||||
|
||||
vec3 m = max(0.5 - vec3(dot(x0,x0), dot(x12.xy,x12.xy), dot(x12.zw,x12.zw)), 0.0);
|
||||
m = m*m ;
|
||||
m = m*m ;
|
||||
|
||||
vec3 x = 2.0 * fract(p * C.www) - 1.0;
|
||||
vec3 h = abs(x) - 0.5;
|
||||
vec3 ox = floor(x + 0.5);
|
||||
vec3 a0 = x - ox;
|
||||
|
||||
m *= 1.79284291400159 - 0.85373472095314 * ( a0*a0 + h*h );
|
||||
|
||||
vec3 g;
|
||||
g.x = a0.x * x0.x + h.x * x0.y;
|
||||
g.yz = a0.yz * x12.xz + h.yz * x12.yw;
|
||||
return 130.0 * dot(m, g);
|
||||
}
|
||||
|
||||
uniform float u_time;
|
||||
uniform vec2 u_resolution;
|
||||
uniform float u_spread;
|
||||
uniform float u_speed;
|
||||
uniform float u_warp;
|
||||
uniform float u_focus;
|
||||
uniform float u_itensity;
|
||||
|
||||
varying vec2 v_pos;
|
||||
|
||||
float circle( in vec2 _pos, in vec2 _origin, in float _radius ) {
|
||||
float SPREAD = 0.7 * u_spread;
|
||||
float SPEED = 0.00055 * u_speed;
|
||||
float WARP = 1.5 * u_warp;
|
||||
float FOCUS = 1.15 * u_focus;
|
||||
|
||||
vec2 dist = _pos - _origin;
|
||||
|
||||
float distortion = snoise( vec2(
|
||||
_pos.x * 1.587 * WARP + u_time * SPEED * 0.5,
|
||||
_pos.y * 1.192 * WARP + u_time * SPEED * 0.3
|
||||
) ) * 0.5 + 0.5;
|
||||
|
||||
float feather = 0.01 + SPREAD * pow( distortion, FOCUS );
|
||||
|
||||
return 1.0 - smoothstep(
|
||||
_radius - ( _radius * feather ),
|
||||
_radius + ( _radius * feather ),
|
||||
dot( dist, dist ) * 4.0
|
||||
);
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec3 green = vec3( 1.0 ) - vec3( 153.0 / 255.0, 211.0 / 255.0, 221.0 / 255.0 );
|
||||
vec3 purple = vec3( 1.0 ) - vec3( 195.0 / 255.0, 165.0 / 255.0, 242.0 / 255.0 );
|
||||
vec3 orange = vec3( 1.0 ) - vec3( 255.0 / 255.0, 156.0 / 255.0, 136.0 / 255.0 );
|
||||
|
||||
float ratio = u_resolution.x / u_resolution.y;
|
||||
|
||||
vec2 uv = vec2( v_pos.x, v_pos.y / ratio ) * 0.5 + 0.5;
|
||||
|
||||
vec3 color = vec3( 0.0 );
|
||||
|
||||
float greenMix = snoise( v_pos * 1.31 + u_time * 0.8 * 0.00017 ) * 0.5 + 0.5;
|
||||
float purpleMix = snoise( v_pos * 1.26 + u_time * 0.8 * -0.0001 ) * 0.5 + 0.5;
|
||||
float orangeMix = snoise( v_pos * 1.34 + u_time * 0.8 * 0.00015 ) * 0.5 + 0.5;
|
||||
|
||||
float alphaOne = 0.35 + 0.65 * pow( snoise( vec2( u_time * 0.00012, uv.x ) ) * 0.5 + 0.5, 1.2 );
|
||||
float alphaTwo = 0.35 + 0.65 * pow( snoise( vec2( ( u_time + 1561.0 ) * 0.00014, uv.x ) ) * 0.5 + 0.5, 1.2 );
|
||||
float alphaThree = 0.35 + 0.65 * pow( snoise( vec2( ( u_time + 3917.0 ) * 0.00013, uv.x ) ) * 0.5 + 0.5, 1.2 );
|
||||
|
||||
color += vec3( circle( uv, vec2( 0.22 + sin( u_time * 0.000201 ) * 0.06, 0.80 + cos( u_time * 0.000151 ) * 0.06 ), 0.15 ) ) * alphaOne * ( purple * purpleMix + orange * orangeMix );
|
||||
color += vec3( circle( uv, vec2( 0.90 + cos( u_time * 0.000166 ) * 0.06, 0.42 + sin( u_time * 0.000138 ) * 0.06 ), 0.18 ) ) * alphaTwo * ( green * greenMix + purple * purpleMix );
|
||||
color += vec3( circle( uv, vec2( 0.19 + sin( u_time * 0.000112 ) * 0.06, 0.25 + sin( u_time * 0.000192 ) * 0.06 ), 0.09 ) ) * alphaThree * ( orange * orangeMix );
|
||||
|
||||
color *= u_itensity + 1.0 * pow( snoise( vec2( v_pos.y + u_time * 0.00013, v_pos.x + u_time * -0.00009 ) ) * 0.5 + 0.5, 2.0 );
|
||||
|
||||
vec3 inverted = vec3( 1.0 ) - color;
|
||||
gl_FragColor = vec4( color, max(max(color.x, color.y), color.z) );
|
||||
}
|
||||
`);
|
||||
|
||||
gl.useProgram(shaderProgram);
|
||||
const u_resolution = gl.getUniformLocation(shaderProgram, 'u_resolution');
|
||||
const u_time = gl.getUniformLocation(shaderProgram, 'u_time');
|
||||
const u_spread = gl.getUniformLocation(shaderProgram, 'u_spread');
|
||||
const u_speed = gl.getUniformLocation(shaderProgram, 'u_speed');
|
||||
const u_warp = gl.getUniformLocation(shaderProgram, 'u_warp');
|
||||
const u_focus = gl.getUniformLocation(shaderProgram, 'u_focus');
|
||||
const u_itensity = gl.getUniformLocation(shaderProgram, 'u_itensity');
|
||||
const u_scale = gl.getUniformLocation(shaderProgram, 'u_scale');
|
||||
gl.uniform2fv(u_resolution, [canvas.width, canvas.height]);
|
||||
gl.uniform1f(u_spread, 1.0);
|
||||
gl.uniform1f(u_speed, 1.0);
|
||||
gl.uniform1f(u_warp, 1.0);
|
||||
gl.uniform1f(u_focus, props.focus);
|
||||
gl.uniform1f(u_itensity, 0.5);
|
||||
gl.uniform2fv(u_scale, [props.scale, props.scale]);
|
||||
|
||||
const vertex = gl.getAttribLocation(shaderProgram, 'vertex');
|
||||
gl.enableVertexAttribArray(vertex);
|
||||
gl.vertexAttribPointer(vertex, 2, gl.FLOAT, false, 0, 0);
|
||||
|
||||
const vertices = [1.0, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0, -1.0];
|
||||
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.DYNAMIC_DRAW);
|
||||
|
||||
if (isChromatic()) {
|
||||
gl!.uniform1f(u_time, 0);
|
||||
gl!.drawArrays(gl!.TRIANGLE_STRIP, 0, 4);
|
||||
} else {
|
||||
function render(timeStamp) {
|
||||
gl!.uniform1f(u_time, timeStamp);
|
||||
gl!.drawArrays(gl!.TRIANGLE_STRIP, 0, 4);
|
||||
|
||||
handle = window.requestAnimationFrame(render);
|
||||
}
|
||||
|
||||
handle = window.requestAnimationFrame(render);
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (handle) {
|
||||
window.cancelAnimationFrame(handle);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
</style>
|
@@ -11,29 +11,29 @@
|
||||
<div v-else-if="c.type === 'buttons'" class="_buttons" :style="{ justifyContent: align }">
|
||||
<MkButton v-for="button in c.buttons" :primary="button.primary" :rounded="button.rounded" :disabled="button.disabled" inline :small="size === 'small'" @click="button.onClick">{{ button.text }}</MkButton>
|
||||
</div>
|
||||
<MkSwitch v-else-if="c.type === 'switch'" :model-value="valueForSwitch" @update:model-value="onSwitchUpdate">
|
||||
<MkSwitch v-else-if="c.type === 'switch'" :modelValue="valueForSwitch" @update:modelValue="onSwitchUpdate">
|
||||
<template v-if="c.label" #label>{{ c.label }}</template>
|
||||
<template v-if="c.caption" #caption>{{ c.caption }}</template>
|
||||
</MkSwitch>
|
||||
<MkTextarea v-else-if="c.type === 'textarea'" :model-value="c.default" @update:model-value="c.onInput">
|
||||
<MkTextarea v-else-if="c.type === 'textarea'" :modelValue="c.default" @update:modelValue="c.onInput">
|
||||
<template v-if="c.label" #label>{{ c.label }}</template>
|
||||
<template v-if="c.caption" #caption>{{ c.caption }}</template>
|
||||
</MkTextarea>
|
||||
<MkInput v-else-if="c.type === 'textInput'" :small="size === 'small'" :model-value="c.default" @update:model-value="c.onInput">
|
||||
<MkInput v-else-if="c.type === 'textInput'" :small="size === 'small'" :modelValue="c.default" @update:modelValue="c.onInput">
|
||||
<template v-if="c.label" #label>{{ c.label }}</template>
|
||||
<template v-if="c.caption" #caption>{{ c.caption }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-else-if="c.type === 'numberInput'" :small="size === 'small'" :model-value="c.default" type="number" @update:model-value="c.onInput">
|
||||
<MkInput v-else-if="c.type === 'numberInput'" :small="size === 'small'" :modelValue="c.default" type="number" @update:modelValue="c.onInput">
|
||||
<template v-if="c.label" #label>{{ c.label }}</template>
|
||||
<template v-if="c.caption" #caption>{{ c.caption }}</template>
|
||||
</MkInput>
|
||||
<MkSelect v-else-if="c.type === 'select'" :small="size === 'small'" :model-value="c.default" @update:model-value="c.onChange">
|
||||
<MkSelect v-else-if="c.type === 'select'" :small="size === 'small'" :modelValue="c.default" @update:modelValue="c.onChange">
|
||||
<template v-if="c.label" #label>{{ c.label }}</template>
|
||||
<template v-if="c.caption" #caption>{{ c.caption }}</template>
|
||||
<option v-for="item in c.items" :key="item.value" :value="item.value">{{ item.text }}</option>
|
||||
</MkSelect>
|
||||
<MkButton v-else-if="c.type === 'postFormButton'" :primary="c.primary" :rounded="c.rounded" :small="size === 'small'" inline @click="openPostForm">{{ c.text }}</MkButton>
|
||||
<MkFolder v-else-if="c.type === 'folder'" :default-open="c.opened">
|
||||
<MkFolder v-else-if="c.type === 'folder'" :defaultOpen="c.opened">
|
||||
<template #label>{{ c.title }}</template>
|
||||
<template v-for="child in c.children" :key="child">
|
||||
<MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size"/>
|
||||
|
@@ -10,7 +10,7 @@
|
||||
</li>
|
||||
<li tabindex="-1" :class="$style.item" @click="chooseUser()" @keydown="onKeydown">{{ i18n.ts.selectUser }}</li>
|
||||
</ol>
|
||||
<ol v-else-if="hashtags.length > 0" ref="suggests" :class="[$style.list, $style.hashtags]">
|
||||
<ol v-else-if="hashtags.length > 0" ref="suggests" :class="$style.list">
|
||||
<li v-for="hashtag in hashtags" tabindex="-1" :class="$style.item" @click="complete(type, hashtag)" @keydown="onKeydown">
|
||||
<span class="name">{{ hashtag }}</span>
|
||||
</li>
|
||||
@@ -42,7 +42,7 @@ import { acct } from '@/filters/user';
|
||||
import * as os from '@/os';
|
||||
import { MFM_TAGS } from '@/scripts/mfm-tags';
|
||||
import { defaultStore } from '@/store';
|
||||
import { emojilist } from '@/scripts/emojilist';
|
||||
import { emojilist, getEmojiName } from '@/scripts/emojilist';
|
||||
import { i18n } from '@/i18n';
|
||||
import { miLocalStorage } from '@/local-storage';
|
||||
import { customEmojis } from '@/custom-emojis';
|
||||
@@ -71,14 +71,14 @@ const emojiDb = computed(() => {
|
||||
url: char2path(x.char),
|
||||
}));
|
||||
|
||||
for (const x of lib) {
|
||||
if (x.keywords) {
|
||||
for (const k of x.keywords) {
|
||||
for (const index of Object.values(defaultStore.state.additionalUnicodeEmojiIndexes)) {
|
||||
for (const [emoji, keywords] of Object.entries(index)) {
|
||||
for (const k of keywords) {
|
||||
unicodeEmojiDB.push({
|
||||
emoji: x.char,
|
||||
emoji: emoji,
|
||||
name: k,
|
||||
aliasOf: x.name,
|
||||
url: char2path(x.char),
|
||||
aliasOf: getEmojiName(emoji)!,
|
||||
url: char2path(emoji),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-for="user in users" :key="user.id" style="display:inline-block;width:32px;height:32px;margin-right:8px;">
|
||||
<MkAvatar :user="user" style="width:32px;height:32px;" indicator link preview/>
|
||||
<MkAvatar :user="user" style="width:32px; height:32px;" indicator link preview/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@@ -2,23 +2,23 @@
|
||||
<button
|
||||
v-if="!link"
|
||||
ref="el" class="_button"
|
||||
:class="[$style.root, { [$style.inline]: inline, [$style.primary]: primary, [$style.gradate]: gradate, [$style.danger]: danger, [$style.rounded]: rounded, [$style.full]: full, [$style.small]: small, [$style.large]: large, [$style.asLike]: asLike }]"
|
||||
:class="[$style.root, { [$style.inline]: inline, [$style.primary]: primary, [$style.gradate]: gradate, [$style.danger]: danger, [$style.rounded]: rounded, [$style.full]: full, [$style.small]: small, [$style.large]: large, [$style.transparent]: transparent, [$style.asLike]: asLike }]"
|
||||
:type="type"
|
||||
@click="emit('click', $event)"
|
||||
@mousedown="onMousedown"
|
||||
>
|
||||
<div ref="ripples" :class="$style.ripples"></div>
|
||||
<div ref="ripples" :class="$style.ripples" :data-children-class="$style.ripple"></div>
|
||||
<div :class="$style.content">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</button>
|
||||
<MkA
|
||||
v-else class="_button"
|
||||
:class="[$style.root, { [$style.inline]: inline, [$style.primary]: primary, [$style.gradate]: gradate, [$style.danger]: danger, [$style.rounded]: rounded, [$style.full]: full, [$style.small]: small, [$style.large]: large, [$style.asLike]: asLike }]"
|
||||
:class="[$style.root, { [$style.inline]: inline, [$style.primary]: primary, [$style.gradate]: gradate, [$style.danger]: danger, [$style.rounded]: rounded, [$style.full]: full, [$style.small]: small, [$style.large]: large, [$style.transparent]: transparent, [$style.asLike]: asLike }]"
|
||||
:to="to"
|
||||
@mousedown="onMousedown"
|
||||
>
|
||||
<div ref="ripples" :class="$style.ripples"></div>
|
||||
<div ref="ripples" :class="$style.ripples" :data-children-class="$style.ripple"></div>
|
||||
<div :class="$style.content">
|
||||
<slot></slot>
|
||||
</div>
|
||||
@@ -26,9 +26,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { nextTick, onMounted, useCssModule } from 'vue';
|
||||
|
||||
const $style = useCssModule();
|
||||
import { nextTick, onMounted } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
type?: 'button' | 'submit' | 'reset';
|
||||
@@ -44,6 +42,7 @@ const props = defineProps<{
|
||||
full?: boolean;
|
||||
small?: boolean;
|
||||
large?: boolean;
|
||||
transparent?: boolean;
|
||||
asLike?: boolean;
|
||||
}>();
|
||||
|
||||
@@ -80,7 +79,7 @@ function onMousedown(evt: MouseEvent): void {
|
||||
const rect = target.getBoundingClientRect();
|
||||
|
||||
const ripple = document.createElement('div');
|
||||
ripple.classList.add($style.ripple);
|
||||
ripple.classList.add(ripples!.dataset.childrenClass!);
|
||||
ripple.style.top = (evt.clientY - rect.top - 1).toString() + 'px';
|
||||
ripple.style.left = (evt.clientX - rect.left - 1).toString() + 'px';
|
||||
|
||||
@@ -194,6 +193,10 @@ function onMousedown(evt: MouseEvent): void {
|
||||
}
|
||||
}
|
||||
|
||||
&.transparent {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&.gradate {
|
||||
font-weight: bold;
|
||||
color: var(--fgOnAccent) !important;
|
||||
|
@@ -1,20 +1,20 @@
|
||||
<template>
|
||||
<button
|
||||
class="hdcaacmi _button"
|
||||
:class="{ wait, active: isFollowing, full }"
|
||||
class="_button"
|
||||
:class="[$style.root, { [$style.wait]: wait, [$style.active]: isFollowing, [$style.full]: full }]"
|
||||
:disabled="wait"
|
||||
@click="onClick"
|
||||
>
|
||||
<template v-if="!wait">
|
||||
<template v-if="isFollowing">
|
||||
<span v-if="full">{{ i18n.ts.unfollow }}</span><i class="ti ti-minus"></i>
|
||||
<span v-if="full" :class="$style.text">{{ i18n.ts.unfollow }}</span><i class="ti ti-minus"></i>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span v-if="full">{{ i18n.ts.follow }}</span><i class="ti ti-plus"></i>
|
||||
<span v-if="full" :class="$style.text">{{ i18n.ts.follow }}</span><i class="ti ti-plus"></i>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span v-if="full">{{ i18n.ts.processing }}</span><MkLoading :em="true"/>
|
||||
<span v-if="full" :class="$style.text">{{ i18n.ts.processing }}</span><MkLoading :em="true"/>
|
||||
</template>
|
||||
</button>
|
||||
</template>
|
||||
@@ -57,8 +57,8 @@ async function onClick() {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.hdcaacmi {
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
@@ -103,7 +103,7 @@ async function onClick() {
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: #fff;
|
||||
color: var(--fgOnAccent);
|
||||
background: var(--accent);
|
||||
|
||||
&:hover {
|
||||
@@ -121,9 +121,9 @@ async function onClick() {
|
||||
cursor: wait !important;
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
> span {
|
||||
margin-right: 6px;
|
||||
}
|
||||
.text {
|
||||
margin-right: 6px;
|
||||
}
|
||||
</style>
|
||||
|
@@ -2,7 +2,7 @@
|
||||
<MkPagination :pagination="pagination">
|
||||
<template #empty>
|
||||
<div class="_fullinfo">
|
||||
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
|
||||
<img :src="infoImageUrl" class="_ghost"/>
|
||||
<div>{{ i18n.ts.notFound }}</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -17,6 +17,7 @@
|
||||
import MkChannelPreview from '@/components/MkChannelPreview.vue';
|
||||
import MkPagination, { Paging } from '@/components/MkPagination.vue';
|
||||
import { i18n } from '@/i18n';
|
||||
import { infoImageUrl } from '@/instance';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
pagination: Paging;
|
||||
@@ -26,6 +27,3 @@ const props = withDefaults(defineProps<{
|
||||
extractor: (item) => item,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
|
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div class="cbbedffa">
|
||||
<div :class="$style.root">
|
||||
<canvas ref="chartEl"></canvas>
|
||||
<MkChartLegend ref="legendEl" style="margin-top: 8px;"/>
|
||||
<div v-if="fetching" class="fetching">
|
||||
<div v-if="fetching" :class="$style.fetching">
|
||||
<MkLoading/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -817,22 +817,22 @@ onMounted(() => {
|
||||
/* eslint-enable id-denylist */
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.cbbedffa {
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
> .fetching {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
-webkit-backdrop-filter: var(--blur, blur(12px));
|
||||
backdrop-filter: var(--blur, blur(12px));
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: wait;
|
||||
}
|
||||
.fetching {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
-webkit-backdrop-filter: var(--blur, blur(12px));
|
||||
backdrop-filter: var(--blur, blur(12px));
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: wait;
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<MkTooltip ref="tooltip" :showing="showing" :x="x" :y="y" :max-width="340" :direction="'top'" :inner-margin="16" @closed="emit('closed')">
|
||||
<MkTooltip ref="tooltip" :showing="showing" :x="x" :y="y" :maxWidth="340" :direction="'top'" :innerMargin="16" @closed="emit('closed')">
|
||||
<div v-if="title || series">
|
||||
<div v-if="title" :class="$style.title">{{ title }}</div>
|
||||
<template v-if="series">
|
||||
|
@@ -3,7 +3,7 @@
|
||||
<div v-if="game.ready" :class="$style.game">
|
||||
<div :class="$style.cps" class="">{{ number(cps) }}cps</div>
|
||||
<div :class="$style.count" class=""><i class="ti ti-cookie" style="font-size: 70%;"></i> {{ number(cookies) }}</div>
|
||||
<button v-click-anime class="_button" :class="$style.button" @click="onClick">
|
||||
<button v-click-anime class="_button" @click="onClick">
|
||||
<img src="/client-assets/cookie.png" :class="$style.img">
|
||||
</button>
|
||||
</div>
|
||||
@@ -84,10 +84,6 @@ onUnmounted(() => {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.button {
|
||||
|
||||
}
|
||||
|
||||
.img {
|
||||
max-width: 90px;
|
||||
}
|
||||
|
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<div ref="rootEl" class="_panel" :class="[$style.root, { [$style.naked]: naked, [$style.thin]: thin, [$style.hideHeader]: !showHeader, [$style.scrollable]: scrollable, [$style.closed]: !showBody }]">
|
||||
<div ref="rootEl" class="_panel" :class="[$style.root, { [$style.naked]: naked, [$style.thin]: thin, [$style.scrollable]: scrollable }]">
|
||||
<header v-if="showHeader" ref="headerEl" :class="$style.header">
|
||||
<div :class="$style.title">
|
||||
<span :class="$style.titleIcon"><slot name="icon"></slot></span>
|
||||
<slot name="header"></slot>
|
||||
</div>
|
||||
<div :class="$style.headerSub">
|
||||
<slot name="func" :button-style-class="$style.headerButton"></slot>
|
||||
<slot name="func" :buttonStyleClass="$style.headerButton"></slot>
|
||||
<button v-if="foldable" :class="$style.headerButton" class="_button" @click="() => showBody = !showBody">
|
||||
<template v-if="showBody"><i class="ti ti-chevron-up"></i></template>
|
||||
<template v-else><i class="ti ti-chevron-down"></i></template>
|
||||
@@ -14,14 +14,14 @@
|
||||
</div>
|
||||
</header>
|
||||
<Transition
|
||||
:enter-active-class="defaultStore.state.animation ? $style.transition_toggle_enterActive : ''"
|
||||
:leave-active-class="defaultStore.state.animation ? $style.transition_toggle_leaveActive : ''"
|
||||
:enter-from-class="defaultStore.state.animation ? $style.transition_toggle_enterFrom : ''"
|
||||
:leave-to-class="defaultStore.state.animation ? $style.transition_toggle_leaveTo : ''"
|
||||
:enterActiveClass="defaultStore.state.animation ? $style.transition_toggle_enterActive : ''"
|
||||
:leaveActiveClass="defaultStore.state.animation ? $style.transition_toggle_leaveActive : ''"
|
||||
:enterFromClass="defaultStore.state.animation ? $style.transition_toggle_enterFrom : ''"
|
||||
:leaveToClass="defaultStore.state.animation ? $style.transition_toggle_leaveTo : ''"
|
||||
@enter="enter"
|
||||
@after-enter="afterEnter"
|
||||
@afterEnter="afterEnter"
|
||||
@leave="leave"
|
||||
@after-leave="afterLeave"
|
||||
@afterLeave="afterLeave"
|
||||
>
|
||||
<div v-show="showBody" ref="contentEl" :class="[$style.content, { [$style.omitted]: omitted }]">
|
||||
<slot></slot>
|
||||
@@ -34,7 +34,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref, shallowRef, watch } from 'vue';
|
||||
import { onMounted, onUnmounted, ref, shallowRef, watch } from 'vue';
|
||||
import { defaultStore } from '@/store';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
@@ -83,13 +83,19 @@ function afterLeave(el) {
|
||||
|
||||
const calcOmit = () => {
|
||||
if (omitted.value || ignoreOmit.value || props.maxHeight == null) return;
|
||||
if (!contentEl.value) return;
|
||||
const height = contentEl.value.offsetHeight;
|
||||
omitted.value = height > props.maxHeight;
|
||||
};
|
||||
|
||||
const omitObserver = new ResizeObserver((entries, observer) => {
|
||||
calcOmit();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
watch(showBody, v => {
|
||||
const headerHeight = props.showHeader ? headerEl.value.offsetHeight : 0;
|
||||
if (!rootEl.value) return;
|
||||
const headerHeight = props.showHeader ? headerEl.value?.offsetHeight ?? 0 : 0;
|
||||
rootEl.value.style.minHeight = `${headerHeight}px`;
|
||||
if (v) {
|
||||
rootEl.value.style.flexBasis = 'auto';
|
||||
@@ -100,13 +106,15 @@ onMounted(() => {
|
||||
immediate: true,
|
||||
});
|
||||
|
||||
rootEl.value.style.setProperty('--maxHeight', props.maxHeight + 'px');
|
||||
if (rootEl.value) rootEl.value.style.setProperty('--maxHeight', props.maxHeight + 'px');
|
||||
|
||||
calcOmit();
|
||||
|
||||
new ResizeObserver((entries, observer) => {
|
||||
calcOmit();
|
||||
}).observe(contentEl.value);
|
||||
if (contentEl.value) omitObserver.observe(contentEl.value);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
omitObserver.disconnect();
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<Transition
|
||||
appear
|
||||
:enter-active-class="defaultStore.state.animation ? $style.transition_fade_enterActive : ''"
|
||||
:leave-active-class="defaultStore.state.animation ? $style.transition_fade_leaveActive : ''"
|
||||
:enter-from-class="defaultStore.state.animation ? $style.transition_fade_enterFrom : ''"
|
||||
:leave-to-class="defaultStore.state.animation ? $style.transition_fade_leaveTo : ''"
|
||||
:enterActiveClass="defaultStore.state.animation ? $style.transition_fade_enterActive : ''"
|
||||
:leaveActiveClass="defaultStore.state.animation ? $style.transition_fade_leaveActive : ''"
|
||||
:enterFromClass="defaultStore.state.animation ? $style.transition_fade_enterFrom : ''"
|
||||
:leaveToClass="defaultStore.state.animation ? $style.transition_fade_leaveTo : ''"
|
||||
>
|
||||
<div ref="rootEl" :class="$style.root" :style="{ zIndex }" @contextmenu.prevent.stop="() => {}">
|
||||
<MkMenu :items="items" :align="'left'" @close="$emit('closed')"/>
|
||||
|
@@ -4,7 +4,7 @@
|
||||
:width="800"
|
||||
:height="500"
|
||||
:scroll="false"
|
||||
:with-ok-button="true"
|
||||
:withOkButton="true"
|
||||
@close="cancel()"
|
||||
@ok="ok()"
|
||||
@closed="$emit('closed')"
|
||||
|
@@ -36,7 +36,7 @@ export default defineComponent({
|
||||
},
|
||||
|
||||
setup(props, { slots, expose }) {
|
||||
const $style = useCssModule();
|
||||
const $style = useCssModule(); // カスタムレンダラなので使っても大丈夫
|
||||
function getDateText(time: string) {
|
||||
const date = new Date(time).getDate();
|
||||
const month = new Date(time).getMonth() + 1;
|
||||
|
@@ -1,10 +1,18 @@
|
||||
<template>
|
||||
<MkModal ref="modal" :prefer-type="'dialog'" :z-priority="'high'" @click="done(true)" @closed="emit('closed')">
|
||||
<MkModal ref="modal" :preferType="'dialog'" :zPriority="'high'" @click="done(true)" @closed="emit('closed')">
|
||||
<div :class="$style.root">
|
||||
<div v-if="icon" :class="$style.icon">
|
||||
<i :class="icon"></i>
|
||||
</div>
|
||||
<div v-else-if="!input && !select" :class="[$style.icon, $style['type_' + type]]">
|
||||
<div
|
||||
v-else-if="!input && !select"
|
||||
:class="[$style.icon, {
|
||||
[$style.type_success]: type === 'success',
|
||||
[$style.type_error]: type === 'error',
|
||||
[$style.type_warning]: type === 'warning',
|
||||
[$style.type_info]: type === 'info',
|
||||
}]"
|
||||
>
|
||||
<i v-if="type === 'success'" :class="$style.iconInner" class="ti ti-check"></i>
|
||||
<i v-else-if="type === 'error'" :class="$style.iconInner" class="ti ti-circle-x"></i>
|
||||
<i v-else-if="type === 'warning'" :class="$style.iconInner" class="ti ti-alert-triangle"></i>
|
||||
|
@@ -0,0 +1,32 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import isChromatic from 'chromatic/isChromatic';
|
||||
import MkDigitalClock from './MkDigitalClock.vue';
|
||||
export const Default = {
|
||||
render(args) {
|
||||
return {
|
||||
components: {
|
||||
MkDigitalClock,
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
args,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
props() {
|
||||
return {
|
||||
...this.args,
|
||||
};
|
||||
},
|
||||
},
|
||||
template: '<MkDigitalClock v-bind="props" />',
|
||||
};
|
||||
},
|
||||
args: {
|
||||
now: isChromatic() ? () => new Date('2023-01-01T10:10:30') : undefined,
|
||||
},
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
} satisfies StoryObj<typeof MkDigitalClock>;
|
@@ -11,19 +11,21 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onUnmounted, ref, watch } from 'vue';
|
||||
import { onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import { defaultIdlingRenderScheduler } from '@/scripts/idle-render.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
showS?: boolean;
|
||||
showMs?: boolean;
|
||||
offset?: number;
|
||||
now?: () => Date;
|
||||
}>(), {
|
||||
showS: true,
|
||||
showMs: false,
|
||||
offset: 0 - new Date().getTimezoneOffset(),
|
||||
now: () => new Date(),
|
||||
});
|
||||
|
||||
let intervalId;
|
||||
const hh = ref('');
|
||||
const mm = ref('');
|
||||
const ss = ref('');
|
||||
@@ -39,9 +41,9 @@ watch(showColon, (v) => {
|
||||
}
|
||||
});
|
||||
|
||||
const tick = () => {
|
||||
const now = new Date();
|
||||
now.setMinutes(now.getMinutes() + (new Date().getTimezoneOffset() + props.offset));
|
||||
const tick = (): void => {
|
||||
const now = props.now();
|
||||
now.setMinutes(now.getMinutes() + now.getTimezoneOffset() + props.offset);
|
||||
hh.value = now.getHours().toString().padStart(2, '0');
|
||||
mm.value = now.getMinutes().toString().padStart(2, '0');
|
||||
ss.value = now.getSeconds().toString().padStart(2, '0');
|
||||
@@ -52,13 +54,12 @@ const tick = () => {
|
||||
|
||||
tick();
|
||||
|
||||
watch(() => props.showMs, () => {
|
||||
if (intervalId) window.clearInterval(intervalId);
|
||||
intervalId = window.setInterval(tick, props.showMs ? 10 : 1000);
|
||||
}, { immediate: true });
|
||||
onMounted(() => {
|
||||
defaultIdlingRenderScheduler.add(tick);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.clearInterval(intervalId);
|
||||
defaultIdlingRenderScheduler.delete(tick);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@@ -1,7 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
class="ncvczrfv"
|
||||
:class="{ isSelected }"
|
||||
:class="[$style.root, { [$style.isSelected]: isSelected }]"
|
||||
draggable="true"
|
||||
:title="title"
|
||||
@click="onClick"
|
||||
@@ -9,25 +8,27 @@
|
||||
@dragstart="onDragstart"
|
||||
@dragend="onDragend"
|
||||
>
|
||||
<div v-if="$i?.avatarId == file.id" class="label">
|
||||
<img src="/client-assets/label.svg"/>
|
||||
<p>{{ i18n.ts.avatar }}</p>
|
||||
</div>
|
||||
<div v-if="$i?.bannerId == file.id" class="label">
|
||||
<img src="/client-assets/label.svg"/>
|
||||
<p>{{ i18n.ts.banner }}</p>
|
||||
</div>
|
||||
<div v-if="file.isSensitive" class="label red">
|
||||
<img src="/client-assets/label-red.svg"/>
|
||||
<p>{{ i18n.ts.nsfw }}</p>
|
||||
</div>
|
||||
<div style="pointer-events: none;">
|
||||
<div v-if="$i?.avatarId == file.id" :class="[$style.label]">
|
||||
<img :class="$style.labelImg" src="/client-assets/label.svg"/>
|
||||
<p :class="$style.labelText">{{ i18n.ts.avatar }}</p>
|
||||
</div>
|
||||
<div v-if="$i?.bannerId == file.id" :class="[$style.label]">
|
||||
<img :class="$style.labelImg" src="/client-assets/label.svg"/>
|
||||
<p :class="$style.labelText">{{ i18n.ts.banner }}</p>
|
||||
</div>
|
||||
<div v-if="file.isSensitive" :class="[$style.label, $style.red]">
|
||||
<img :class="$style.labelImg" src="/client-assets/label-red.svg"/>
|
||||
<p :class="$style.labelText">{{ i18n.ts.nsfw }}</p>
|
||||
</div>
|
||||
|
||||
<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/>
|
||||
<MkDriveFileThumbnail :class="$style.thumbnail" :file="file" fit="contain"/>
|
||||
|
||||
<p class="name">
|
||||
<span>{{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }}</span>
|
||||
<span v-if="file.name.lastIndexOf('.') != -1" class="ext">{{ file.name.substr(file.name.lastIndexOf('.')) }}</span>
|
||||
</p>
|
||||
<p :class="$style.name">
|
||||
<span>{{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }}</span>
|
||||
<span v-if="file.name.lastIndexOf('.') != -1" style="opacity: 0.5;">{{ file.name.substr(file.name.lastIndexOf('.')) }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -88,20 +89,13 @@ function onDragend() {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.ncvczrfv {
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
position: relative;
|
||||
padding: 8px 0 0 0;
|
||||
min-height: 180px;
|
||||
border-radius: 8px;
|
||||
|
||||
&, * {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
> * {
|
||||
pointer-events: none;
|
||||
}
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: rgba(#000, 0.05);
|
||||
@@ -165,82 +159,78 @@ function onDragend() {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .label {
|
||||
.label {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
|
||||
&:before,
|
||||
&:after {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
background: #0c7ac9;
|
||||
}
|
||||
|
||||
&:before {
|
||||
top: 0;
|
||||
left: 57px;
|
||||
width: 28px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
&:after {
|
||||
top: 57px;
|
||||
left: 0;
|
||||
width: 8px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
&.red {
|
||||
&:before,
|
||||
&:after {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
background: #0c7ac9;
|
||||
}
|
||||
|
||||
&:before {
|
||||
top: 0;
|
||||
left: 57px;
|
||||
width: 28px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
&:after {
|
||||
top: 57px;
|
||||
left: 0;
|
||||
width: 8px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
&.red {
|
||||
&:before,
|
||||
&:after {
|
||||
background: #c12113;
|
||||
}
|
||||
}
|
||||
|
||||
> img {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
> p {
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
top: 19px;
|
||||
left: -28px;
|
||||
width: 120px;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
line-height: 28px;
|
||||
color: #fff;
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
}
|
||||
|
||||
> .thumbnail {
|
||||
width: 110px;
|
||||
height: 110px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
> .name {
|
||||
display: block;
|
||||
margin: 4px 0 0 0;
|
||||
font-size: 0.8em;
|
||||
text-align: center;
|
||||
word-break: break-all;
|
||||
color: var(--fg);
|
||||
overflow: hidden;
|
||||
|
||||
> .ext {
|
||||
opacity: 0.5;
|
||||
background: #c12113;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.labelImg {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.labelText {
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
top: 19px;
|
||||
left: -28px;
|
||||
width: 120px;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
line-height: 28px;
|
||||
color: #fff;
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
|
||||
.thumbnail {
|
||||
width: 110px;
|
||||
height: 110px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.name {
|
||||
display: block;
|
||||
margin: 4px 0 0 0;
|
||||
font-size: 0.8em;
|
||||
text-align: center;
|
||||
word-break: break-all;
|
||||
color: var(--fg);
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,7 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
class="rghtznwe"
|
||||
:class="{ draghover }"
|
||||
:class="[$style.root, { [$style.draghover]: draghover }]"
|
||||
draggable="true"
|
||||
:title="title"
|
||||
@click="onClick"
|
||||
@@ -15,15 +14,15 @@
|
||||
@dragstart="onDragstart"
|
||||
@dragend="onDragend"
|
||||
>
|
||||
<p class="name">
|
||||
<template v-if="hover"><i class="ti ti-folder ti-fw"></i></template>
|
||||
<template v-if="!hover"><i class="ti ti-folder ti-fw"></i></template>
|
||||
<p :class="$style.name">
|
||||
<template v-if="hover"><i :class="$style.icon" class="ti ti-folder ti-fw"></i></template>
|
||||
<template v-if="!hover"><i :class="$style.icon" class="ti ti-folder ti-fw"></i></template>
|
||||
{{ folder.name }}
|
||||
</p>
|
||||
<p v-if="defaultStore.state.uploadFolder == folder.id" class="upload">
|
||||
<p v-if="defaultStore.state.uploadFolder == folder.id" :class="$style.upload">
|
||||
{{ i18n.ts.uploadFolder }}
|
||||
</p>
|
||||
<button v-if="selectMode" class="checkbox _button" :class="{ checked: isSelected }" @click.prevent.stop="checkboxClicked"></button>
|
||||
<button v-if="selectMode" class="_button" :class="[$style.checkbox, { [$style.checked]: isSelected }]" @click.prevent.stop="checkboxClicked"></button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -267,35 +266,14 @@ function onContextmenu(ev: MouseEvent) {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.rghtznwe {
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
position: relative;
|
||||
padding: 8px;
|
||||
height: 64px;
|
||||
background: var(--driveFolderBg);
|
||||
border-radius: 4px;
|
||||
|
||||
&, * {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
*:not(.checkbox) {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
> .checkbox {
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
right: 8px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: #fff;
|
||||
border: solid 1px #000;
|
||||
|
||||
&.checked {
|
||||
background: var(--accent);
|
||||
}
|
||||
}
|
||||
cursor: pointer;
|
||||
|
||||
&.draghover {
|
||||
&:after {
|
||||
@@ -310,24 +288,38 @@ function onContextmenu(ev: MouseEvent) {
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .name {
|
||||
margin: 0;
|
||||
font-size: 0.9em;
|
||||
color: var(--desktopDriveFolderFg);
|
||||
.checkbox {
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
right: 8px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: #fff;
|
||||
border: solid 1px #000;
|
||||
|
||||
> i {
|
||||
margin-right: 4px;
|
||||
margin-left: 2px;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
> .upload {
|
||||
margin: 4px 4px;
|
||||
font-size: 0.8em;
|
||||
text-align: right;
|
||||
color: var(--desktopDriveFolderFg);
|
||||
&.checked {
|
||||
background: var(--accent);
|
||||
}
|
||||
}
|
||||
|
||||
.name {
|
||||
margin: 0;
|
||||
font-size: 0.9em;
|
||||
color: var(--desktopDriveFolderFg);
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-right: 4px;
|
||||
margin-left: 2px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.upload {
|
||||
margin: 4px 4px;
|
||||
font-size: 0.8em;
|
||||
text-align: right;
|
||||
color: var(--desktopDriveFolderFg);
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<div class="drylbebk"
|
||||
:class="{ draghover }"
|
||||
<div
|
||||
:class="[$style.root, { [$style.draghover]: draghover }]"
|
||||
@click="onClick"
|
||||
@dragover.prevent.stop="onDragover"
|
||||
@dragenter="onDragenter"
|
||||
@dragleave="onDragleave"
|
||||
@drop.stop="onDrop"
|
||||
>
|
||||
<i v-if="folder == null" class="ti ti-cloud"></i>
|
||||
<i v-if="folder == null" class="ti ti-cloud" style="margin-right: 4px;"></i>
|
||||
<span>{{ folder == null ? i18n.ts.drive : folder.name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
@@ -130,18 +130,10 @@ function onDrop(ev: DragEvent) {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.drylbebk {
|
||||
> * {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
&.draghover {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
> i {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,89 +1,90 @@
|
||||
<template>
|
||||
<div class="yfudmmck">
|
||||
<nav>
|
||||
<div class="path" @contextmenu.prevent.stop="() => {}">
|
||||
<div :class="$style.root">
|
||||
<nav :class="$style.nav">
|
||||
<div :class="$style.navPath" @contextmenu.prevent.stop="() => {}">
|
||||
<XNavFolder
|
||||
:class="{ current: folder == null }"
|
||||
:parent-folder="folder"
|
||||
:class="[$style.navPathItem, { [$style.navCurrent]: folder == null }]"
|
||||
:parentFolder="folder"
|
||||
@move="move"
|
||||
@upload="upload"
|
||||
@remove-file="removeFile"
|
||||
@remove-folder="removeFolder"
|
||||
@removeFile="removeFile"
|
||||
@removeFolder="removeFolder"
|
||||
/>
|
||||
<template v-for="f in hierarchyFolders">
|
||||
<span class="separator"><i class="ti ti-chevron-right"></i></span>
|
||||
<span :class="[$style.navPathItem, $style.navSeparator]"><i class="ti ti-chevron-right"></i></span>
|
||||
<XNavFolder
|
||||
:folder="f"
|
||||
:parent-folder="folder"
|
||||
:parentFolder="folder"
|
||||
:class="[$style.navPathItem]"
|
||||
@move="move"
|
||||
@upload="upload"
|
||||
@remove-file="removeFile"
|
||||
@remove-folder="removeFolder"
|
||||
@removeFile="removeFile"
|
||||
@removeFolder="removeFolder"
|
||||
/>
|
||||
</template>
|
||||
<span v-if="folder != null" class="separator"><i class="ti ti-chevron-right"></i></span>
|
||||
<span v-if="folder != null" class="folder current">{{ folder.name }}</span>
|
||||
<span v-if="folder != null" :class="[$style.navPathItem, $style.navSeparator]"><i class="ti ti-chevron-right"></i></span>
|
||||
<span v-if="folder != null" :class="[$style.navPathItem, $style.navCurrent]">{{ folder.name }}</span>
|
||||
</div>
|
||||
<button class="menu _button" @click="showMenu"><i class="ti ti-dots"></i></button>
|
||||
<button class="_button" :class="$style.navMenu" @click="showMenu"><i class="ti ti-dots"></i></button>
|
||||
</nav>
|
||||
<div
|
||||
ref="main" class="main"
|
||||
:class="{ uploading: uploadings.length > 0, fetching }"
|
||||
ref="main"
|
||||
:class="[$style.main, { [$style.uploading]: uploadings.length > 0, [$style.fetching]: fetching }]"
|
||||
@dragover.prevent.stop="onDragover"
|
||||
@dragenter="onDragenter"
|
||||
@dragleave="onDragleave"
|
||||
@drop.prevent.stop="onDrop"
|
||||
@contextmenu.stop="onContextmenu"
|
||||
>
|
||||
<div ref="contents" class="contents">
|
||||
<div v-show="folders.length > 0" ref="foldersContainer" class="folders">
|
||||
<div ref="contents">
|
||||
<div v-show="folders.length > 0" ref="foldersContainer" :class="$style.folders">
|
||||
<XFolder
|
||||
v-for="(f, i) in folders"
|
||||
:key="f.id"
|
||||
v-anim="i"
|
||||
class="folder"
|
||||
:class="$style.folder"
|
||||
:folder="f"
|
||||
:select-mode="select === 'folder'"
|
||||
:is-selected="selectedFolders.some(x => x.id === f.id)"
|
||||
:selectMode="select === 'folder'"
|
||||
:isSelected="selectedFolders.some(x => x.id === f.id)"
|
||||
@chosen="chooseFolder"
|
||||
@move="move"
|
||||
@upload="upload"
|
||||
@remove-file="removeFile"
|
||||
@remove-folder="removeFolder"
|
||||
@removeFile="removeFile"
|
||||
@removeFolder="removeFolder"
|
||||
@dragstart="isDragSource = true"
|
||||
@dragend="isDragSource = false"
|
||||
/>
|
||||
<!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid -->
|
||||
<div v-for="(n, i) in 16" :key="i" class="padding"></div>
|
||||
<div v-for="(n, i) in 16" :key="i" :class="$style.padding"></div>
|
||||
<MkButton v-if="moreFolders" ref="moreFolders">{{ i18n.ts.loadMore }}</MkButton>
|
||||
</div>
|
||||
<div v-show="files.length > 0" ref="filesContainer" class="files">
|
||||
<div v-show="files.length > 0" ref="filesContainer" :class="$style.files">
|
||||
<XFile
|
||||
v-for="(file, i) in files"
|
||||
:key="file.id"
|
||||
v-anim="i"
|
||||
class="file"
|
||||
:class="$style.file"
|
||||
:file="file"
|
||||
:select-mode="select === 'file'"
|
||||
:is-selected="selectedFiles.some(x => x.id === file.id)"
|
||||
:selectMode="select === 'file'"
|
||||
:isSelected="selectedFiles.some(x => x.id === file.id)"
|
||||
@chosen="chooseFile"
|
||||
@dragstart="isDragSource = true"
|
||||
@dragend="isDragSource = false"
|
||||
/>
|
||||
<!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid -->
|
||||
<div v-for="(n, i) in 16" :key="i" class="padding"></div>
|
||||
<div v-for="(n, i) in 16" :key="i" :class="$style.padding"></div>
|
||||
<MkButton v-show="moreFiles" ref="loadMoreFiles" @click="fetchMoreFiles">{{ i18n.ts.loadMore }}</MkButton>
|
||||
</div>
|
||||
<div v-if="files.length == 0 && folders.length == 0 && !fetching" class="empty">
|
||||
<p v-if="draghover">{{ i18n.t('empty-draghover') }}</p>
|
||||
<p v-if="!draghover && folder == null"><strong>{{ i18n.ts.emptyDrive }}</strong><br/>{{ i18n.t('empty-drive-description') }}</p>
|
||||
<p v-if="!draghover && folder != null">{{ i18n.ts.emptyFolder }}</p>
|
||||
<div v-if="files.length == 0 && folders.length == 0 && !fetching" :class="$style.empty">
|
||||
<div v-if="draghover">{{ i18n.t('empty-draghover') }}</div>
|
||||
<div v-if="!draghover && folder == null"><strong>{{ i18n.ts.emptyDrive }}</strong><br/>{{ i18n.t('empty-drive-description') }}</div>
|
||||
<div v-if="!draghover && folder != null">{{ i18n.ts.emptyFolder }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<MkLoading v-if="fetching"/>
|
||||
</div>
|
||||
<div v-if="draghover" class="dropzone"></div>
|
||||
<input ref="fileInput" type="file" accept="*/*" multiple tabindex="-1" @change="onChangeFileInput"/>
|
||||
<div v-if="draghover" :class="$style.dropzone"></div>
|
||||
<input ref="fileInput" style="display: none;" type="file" accept="*/*" multiple tabindex="-1" @change="onChangeFileInput"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -95,7 +96,7 @@ import XNavFolder from '@/components/MkDrive.navFolder.vue';
|
||||
import XFolder from '@/components/MkDrive.folder.vue';
|
||||
import XFile from '@/components/MkDrive.file.vue';
|
||||
import * as os from '@/os';
|
||||
import { stream } from '@/stream';
|
||||
import { useStream } from '@/stream';
|
||||
import { defaultStore } from '@/store';
|
||||
import { i18n } from '@/i18n';
|
||||
import { uploadFile, uploads } from '@/scripts/upload';
|
||||
@@ -131,7 +132,7 @@ const hierarchyFolders = ref<Misskey.entities.DriveFolder[]>([]);
|
||||
const selectedFiles = ref<Misskey.entities.DriveFile[]>([]);
|
||||
const selectedFolders = ref<Misskey.entities.DriveFolder[]>([]);
|
||||
const uploadings = uploads;
|
||||
const connection = stream.useChannel('drive');
|
||||
const connection = useStream().useChannel('drive');
|
||||
const keepOriginal = ref<boolean>(defaultStore.state.keepOriginalUploading); // 外部渡しが多いので$refは使わないほうがよい
|
||||
|
||||
// ドロップされようとしているか
|
||||
@@ -658,147 +659,116 @@ onBeforeUnmount(() => {
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.yfudmmck {
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
> nav {
|
||||
display: flex;
|
||||
z-index: 2;
|
||||
width: 100%;
|
||||
padding: 0 8px;
|
||||
box-sizing: border-box;
|
||||
overflow: auto;
|
||||
font-size: 0.9em;
|
||||
box-shadow: 0 1px 0 var(--divider);
|
||||
.nav {
|
||||
display: flex;
|
||||
z-index: 2;
|
||||
width: 100%;
|
||||
padding: 0 8px;
|
||||
box-sizing: border-box;
|
||||
overflow: auto;
|
||||
font-size: 0.9em;
|
||||
box-shadow: 0 1px 0 var(--divider);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
&, * {
|
||||
user-select: none;
|
||||
}
|
||||
.navPath {
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
line-height: 42px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
> .path {
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
line-height: 42px;
|
||||
white-space: nowrap;
|
||||
.navPathItem {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
padding: 0 8px;
|
||||
line-height: 42px;
|
||||
cursor: pointer;
|
||||
|
||||
> * {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
padding: 0 8px;
|
||||
line-height: 42px;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
* {
|
||||
pointer-events: none;
|
||||
}
|
||||
&.navCurrent {
|
||||
font-weight: bold;
|
||||
cursor: default;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
&.current {
|
||||
font-weight: bold;
|
||||
cursor: default;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.separator {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
opacity: 0.5;
|
||||
cursor: default;
|
||||
|
||||
> i {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .menu {
|
||||
margin-left: auto;
|
||||
padding: 0 12px;
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
> .main {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: var(--margin);
|
||||
|
||||
&, * {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
&.fetching {
|
||||
cursor: wait !important;
|
||||
|
||||
* {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
> .contents {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
&.uploading {
|
||||
height: calc(100% - 38px - 100px);
|
||||
}
|
||||
|
||||
> .contents {
|
||||
|
||||
> .folders,
|
||||
> .files {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
> .folder,
|
||||
> .file {
|
||||
flex-grow: 1;
|
||||
width: 128px;
|
||||
margin: 4px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
> .padding {
|
||||
flex-grow: 1;
|
||||
pointer-events: none;
|
||||
width: 128px + 8px;
|
||||
}
|
||||
}
|
||||
|
||||
> .empty {
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
|
||||
> p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.navSeparator {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
opacity: 0.5;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
> .dropzone {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 38px;
|
||||
width: 100%;
|
||||
height: calc(100% - 38px);
|
||||
border: dashed 2px var(--focus);
|
||||
.navMenu {
|
||||
margin-left: auto;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.main {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: var(--margin);
|
||||
user-select: none;
|
||||
|
||||
&.fetching {
|
||||
cursor: wait !important;
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
> input {
|
||||
display: none;
|
||||
&.uploading {
|
||||
height: calc(100% - 38px - 100px);
|
||||
}
|
||||
}
|
||||
|
||||
.folders,
|
||||
.files {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.folder,
|
||||
.file {
|
||||
flex-grow: 1;
|
||||
width: 128px;
|
||||
margin: 4px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.padding {
|
||||
flex-grow: 1;
|
||||
pointer-events: none;
|
||||
width: 128px + 8px;
|
||||
}
|
||||
|
||||
.empty {
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.dropzone {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 38px;
|
||||
width: 100%;
|
||||
height: calc(100% - 38px);
|
||||
border: dashed 2px var(--focus);
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,16 +1,16 @@
|
||||
<template>
|
||||
<div ref="thumbnail" class="zdjebgpv">
|
||||
<div ref="thumbnail" :class="$style.root">
|
||||
<ImgWithBlurhash v-if="isThumbnailAvailable" :hash="file.blurhash" :src="file.thumbnailUrl" :alt="file.name" :title="file.name" :cover="fit !== 'contain'"/>
|
||||
<i v-else-if="is === 'image'" class="ti ti-photo icon"></i>
|
||||
<i v-else-if="is === 'video'" class="ti ti-video icon"></i>
|
||||
<i v-else-if="is === 'audio' || is === 'midi'" class="ti ti-file-music icon"></i>
|
||||
<i v-else-if="is === 'csv'" class="ti ti-file-text icon"></i>
|
||||
<i v-else-if="is === 'pdf'" class="ti ti-file-text icon"></i>
|
||||
<i v-else-if="is === 'textfile'" class="ti ti-file-text icon"></i>
|
||||
<i v-else-if="is === 'archive'" class="ti ti-file-zip icon"></i>
|
||||
<i v-else class="ti ti-file icon"></i>
|
||||
<i v-else-if="is === 'image'" class="ti ti-photo" :class="$style.icon"></i>
|
||||
<i v-else-if="is === 'video'" class="ti ti-video" :class="$style.icon"></i>
|
||||
<i v-else-if="is === 'audio' || is === 'midi'" class="ti ti-file-music" :class="$style.icon"></i>
|
||||
<i v-else-if="is === 'csv'" class="ti ti-file-text" :class="$style.icon"></i>
|
||||
<i v-else-if="is === 'pdf'" class="ti ti-file-text" :class="$style.icon"></i>
|
||||
<i v-else-if="is === 'textfile'" class="ti ti-file-text" :class="$style.icon"></i>
|
||||
<i v-else-if="is === 'archive'" class="ti ti-file-zip" :class="$style.icon"></i>
|
||||
<i v-else class="ti ti-file" :class="$style.icon"></i>
|
||||
|
||||
<i v-if="isThumbnailAvailable && is === 'video'" class="ti ti-video icon-sub"></i>
|
||||
<i v-if="isThumbnailAvailable && is === 'video'" class="ti ti-video" :class="$style.iconSub"></i>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -53,28 +53,28 @@ const isThumbnailAvailable = computed(() => {
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.zdjebgpv {
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
position: relative;
|
||||
display: flex;
|
||||
background: var(--panel);
|
||||
border-radius: 8px;
|
||||
overflow: clip;
|
||||
}
|
||||
|
||||
> .icon-sub {
|
||||
position: absolute;
|
||||
width: 30%;
|
||||
height: auto;
|
||||
margin: 0;
|
||||
right: 4%;
|
||||
bottom: 4%;
|
||||
}
|
||||
.iconSub {
|
||||
position: absolute;
|
||||
width: 30%;
|
||||
height: auto;
|
||||
margin: 0;
|
||||
right: 4%;
|
||||
bottom: 4%;
|
||||
}
|
||||
|
||||
> .icon {
|
||||
pointer-events: none;
|
||||
margin: auto;
|
||||
font-size: 32px;
|
||||
color: #777;
|
||||
}
|
||||
.icon {
|
||||
pointer-events: none;
|
||||
margin: auto;
|
||||
font-size: 32px;
|
||||
color: #777;
|
||||
}
|
||||
</style>
|
||||
|
@@ -3,8 +3,8 @@
|
||||
ref="dialog"
|
||||
:width="800"
|
||||
:height="500"
|
||||
:with-ok-button="true"
|
||||
:ok-button-disabled="(type === 'file') && (selected.length === 0)"
|
||||
:withOkButton="true"
|
||||
:okButtonDisabled="(type === 'file') && (selected.length === 0)"
|
||||
@click="cancel()"
|
||||
@close="cancel()"
|
||||
@ok="ok()"
|
||||
@@ -14,7 +14,7 @@
|
||||
{{ multiple ? ((type === 'file') ? i18n.ts.selectFiles : i18n.ts.selectFolders) : ((type === 'file') ? i18n.ts.selectFile : i18n.ts.selectFolder) }}
|
||||
<span v-if="selected.length > 0" style="margin-left: 8px; opacity: 0.5;">({{ number(selected.length) }})</span>
|
||||
</template>
|
||||
<XDrive :multiple="multiple" :select="type" @change-selection="onChangeSelection" @selected="ok()"/>
|
||||
<XDrive :multiple="multiple" :select="type" @changeSelection="onChangeSelection" @selected="ok()"/>
|
||||
</MkModalWindow>
|
||||
</template>
|
||||
|
||||
|
@@ -1,15 +1,15 @@
|
||||
<template>
|
||||
<MkWindow
|
||||
ref="window"
|
||||
:initial-width="800"
|
||||
:initial-height="500"
|
||||
:can-resize="true"
|
||||
:initialWidth="800"
|
||||
:initialHeight="500"
|
||||
:canResize="true"
|
||||
@closed="emit('closed')"
|
||||
>
|
||||
<template #header>
|
||||
{{ i18n.ts.drive }}
|
||||
</template>
|
||||
<XDrive :initial-folder="initialFolder"/>
|
||||
<XDrive :initialFolder="initialFolder"/>
|
||||
</MkWindow>
|
||||
</template>
|
||||
|
||||
|
@@ -1,7 +1,8 @@
|
||||
<template>
|
||||
<div class="omfetrab" :class="['s' + size, 'w' + width, 'h' + height, { asDrawer, asWindow }]" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }">
|
||||
<input ref="searchEl" :value="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="i18n.ts.search" type="search" @input="input()" @paste.stop="paste" @keydown.stop.prevent.enter="onEnter">
|
||||
<div ref="emojisEl" class="emojis">
|
||||
<!-- FirefoxのTabフォーカスが想定外の挙動となるためtabindex="-1"を追加 https://github.com/misskey-dev/misskey/issues/10744 -->
|
||||
<div ref="emojisEl" class="emojis" tabindex="-1">
|
||||
<section class="result">
|
||||
<div v-if="searchResultCustom.length > 0" class="body">
|
||||
<button
|
||||
@@ -69,8 +70,8 @@
|
||||
<XSection
|
||||
v-for="category in customEmojiCategories"
|
||||
:key="`custom:${category}`"
|
||||
:initial-shown="false"
|
||||
:emojis="computed(() => customEmojis.filter(e => category === null ? (e.category === 'null' || !e.category) : e.category === category).map(e => `:${e.name}:`))"
|
||||
:initialShown="false"
|
||||
:emojis="computed(() => customEmojis.filter(e => category === null ? (e.category === 'null' || !e.category) : e.category === category).filter(filterAvailable).map(e => `:${e.name}:`))"
|
||||
@chosen="chosen"
|
||||
>
|
||||
{{ category || i18n.ts.other }}
|
||||
@@ -101,7 +102,8 @@ import { isTouchUsing } from '@/scripts/touch';
|
||||
import { deviceKind } from '@/scripts/device-kind';
|
||||
import { i18n } from '@/i18n';
|
||||
import { defaultStore } from '@/store';
|
||||
import { customEmojiCategories, customEmojis } from '@/custom-emojis';
|
||||
import { customEmojiCategories, customEmojis, customEmojisMap } from '@/custom-emojis';
|
||||
import { $i } from '@/account';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
showPinned?: boolean;
|
||||
@@ -222,7 +224,6 @@ watch(q, () => {
|
||||
if (newQ.includes(' ')) { // AND検索
|
||||
const keywords = newQ.split(' ');
|
||||
|
||||
// 名前にキーワードが含まれている
|
||||
for (const emoji of emojis) {
|
||||
if (keywords.every(keyword => emoji.name.includes(keyword))) {
|
||||
matches.add(emoji);
|
||||
@@ -231,11 +232,12 @@ watch(q, () => {
|
||||
}
|
||||
if (matches.size >= max) return matches;
|
||||
|
||||
// 名前またはエイリアスにキーワードが含まれている
|
||||
for (const emoji of emojis) {
|
||||
if (keywords.every(keyword => emoji.name.includes(keyword) || emoji.keywords.some(alias => alias.includes(keyword)))) {
|
||||
matches.add(emoji);
|
||||
if (matches.size >= max) break;
|
||||
for (const index of Object.values(defaultStore.state.additionalUnicodeEmojiIndexes)) {
|
||||
for (const emoji of emojis) {
|
||||
if (keywords.every(keyword => index[emoji.char].some(k => k.includes(keyword)))) {
|
||||
matches.add(emoji);
|
||||
if (matches.size >= max) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -247,13 +249,14 @@ watch(q, () => {
|
||||
}
|
||||
if (matches.size >= max) return matches;
|
||||
|
||||
for (const emoji of emojis) {
|
||||
if (emoji.keywords.some(keyword => keyword.startsWith(newQ))) {
|
||||
matches.add(emoji);
|
||||
if (matches.size >= max) break;
|
||||
for (const index of Object.values(defaultStore.state.additionalUnicodeEmojiIndexes)) {
|
||||
for (const emoji of emojis) {
|
||||
if (index[emoji.char].some(k => k.startsWith(newQ))) {
|
||||
matches.add(emoji);
|
||||
if (matches.size >= max) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (matches.size >= max) return matches;
|
||||
|
||||
for (const emoji of emojis) {
|
||||
if (emoji.name.includes(newQ)) {
|
||||
@@ -263,10 +266,12 @@ watch(q, () => {
|
||||
}
|
||||
if (matches.size >= max) return matches;
|
||||
|
||||
for (const emoji of emojis) {
|
||||
if (emoji.keywords.some(keyword => keyword.includes(newQ))) {
|
||||
matches.add(emoji);
|
||||
if (matches.size >= max) break;
|
||||
for (const index of Object.values(defaultStore.state.additionalUnicodeEmojiIndexes)) {
|
||||
for (const emoji of emojis) {
|
||||
if (index[emoji.char].some(k => k.includes(newQ))) {
|
||||
matches.add(emoji);
|
||||
if (matches.size >= max) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -274,10 +279,14 @@ watch(q, () => {
|
||||
return matches;
|
||||
};
|
||||
|
||||
searchResultCustom.value = Array.from(searchCustom());
|
||||
searchResultCustom.value = Array.from(searchCustom()).filter(filterAvailable);
|
||||
searchResultUnicode.value = Array.from(searchUnicode());
|
||||
});
|
||||
|
||||
function filterAvailable(emoji: Misskey.entities.CustomEmoji): boolean {
|
||||
return (emoji.roleIdsThatCanBeUsedThisEmojiAsReaction == null || emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length === 0) || ($i && $i.roles.some(r => emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.includes(r.id)));
|
||||
}
|
||||
|
||||
function focus() {
|
||||
if (!['smartphone', 'tablet'].includes(deviceKind) && !isTouchUsing) {
|
||||
searchEl.value?.focus({
|
||||
@@ -347,7 +356,7 @@ function done(query?: string): boolean | void {
|
||||
if (query == null || typeof query !== 'string') return;
|
||||
|
||||
const q2 = query.replace(/:/g, '');
|
||||
const exactMatchCustom = customEmojis.value.find(emoji => emoji.name === q2);
|
||||
const exactMatchCustom = customEmojisMap.get(q2);
|
||||
if (exactMatchCustom) {
|
||||
chosen(exactMatchCustom);
|
||||
return true;
|
||||
|
@@ -2,10 +2,10 @@
|
||||
<MkModal
|
||||
ref="modal"
|
||||
v-slot="{ type, maxHeight }"
|
||||
:z-priority="'middle'"
|
||||
:prefer-type="asReactionPicker && defaultStore.state.reactionPickerUseDrawerForMobile === false ? 'popup' : 'auto'"
|
||||
:transparent-bg="true"
|
||||
:manual-showing="manualShowing"
|
||||
:zPriority="'middle'"
|
||||
:preferType="asReactionPicker && defaultStore.state.reactionPickerUseDrawerForMobile === false ? 'popup' : 'auto'"
|
||||
:transparentBg="true"
|
||||
:manualShowing="manualShowing"
|
||||
:src="src"
|
||||
@click="modal?.close()"
|
||||
@opening="opening"
|
||||
@@ -14,11 +14,11 @@
|
||||
>
|
||||
<MkEmojiPicker
|
||||
ref="picker"
|
||||
class="ryghynhb _popup _shadow"
|
||||
:class="{ drawer: type === 'drawer' }"
|
||||
:show-pinned="showPinned"
|
||||
:as-reaction-picker="asReactionPicker"
|
||||
:as-drawer="type === 'drawer'"
|
||||
class="_popup _shadow"
|
||||
:class="{ [$style.drawer]: type === 'drawer' }"
|
||||
:showPinned="showPinned"
|
||||
:asReactionPicker="asReactionPicker"
|
||||
:asDrawer="type === 'drawer'"
|
||||
:max-height="maxHeight"
|
||||
@chosen="chosen"
|
||||
/>
|
||||
@@ -67,12 +67,10 @@ function opening() {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.ryghynhb {
|
||||
&.drawer {
|
||||
border-radius: 24px;
|
||||
border-bottom-right-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
<style lang="scss" module>
|
||||
.drawer {
|
||||
border-radius: 24px;
|
||||
border-bottom-right-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,13 +1,14 @@
|
||||
<template>
|
||||
<MkWindow ref="window"
|
||||
:initial-width="300"
|
||||
:initial-height="290"
|
||||
:can-resize="true"
|
||||
<MkWindow
|
||||
ref="window"
|
||||
:initialWidth="300"
|
||||
:initialHeight="290"
|
||||
:canResize="true"
|
||||
:mini="true"
|
||||
:front="true"
|
||||
@closed="emit('closed')"
|
||||
>
|
||||
<MkEmojiPicker :show-pinned="showPinned" :as-reaction-picker="asReactionPicker" as-window :class="$style.picker" @chosen="chosen"/>
|
||||
<MkEmojiPicker :showPinned="showPinned" :asReactionPicker="asReactionPicker" asWindow :class="$style.picker" @chosen="chosen"/>
|
||||
</MkWindow>
|
||||
</template>
|
||||
|
||||
|
@@ -3,14 +3,14 @@
|
||||
ref="dialog"
|
||||
:width="400"
|
||||
:height="450"
|
||||
:with-ok-button="true"
|
||||
:ok-button-disabled="false"
|
||||
:withOkButton="true"
|
||||
:okButtonDisabled="false"
|
||||
@ok="ok()"
|
||||
@close="dialog.close()"
|
||||
@closed="emit('closed')"
|
||||
>
|
||||
<template #header>{{ i18n.ts.describeFile }}</template>
|
||||
<MkSpacer :margin-min="20" :margin-max="28">
|
||||
<MkSpacer :marginMin="20" :marginMax="28">
|
||||
<MkDriveFileThumbnail :file="file" fit="contain" style="height: 100px; margin-bottom: 16px;"/>
|
||||
<MkTextarea v-model="caption" autofocus :placeholder="i18n.ts.inputNewDescription">
|
||||
<template #label>{{ i18n.ts.caption }}</template>
|
||||
|
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<div class="ssazuxis">
|
||||
<header class="_button" :style="{ background: bg }" @click="showBody = !showBody">
|
||||
<div class="title"><div><slot name="header"></slot></div></div>
|
||||
<div class="divider"></div>
|
||||
<button class="_button">
|
||||
<div ref="el" :class="$style.root">
|
||||
<header :class="$style.header" class="_button" :style="{ background: bg }" @click="showBody = !showBody">
|
||||
<div :class="$style.title"><div><slot name="header"></slot></div></div>
|
||||
<div :class="$style.divider"></div>
|
||||
<button class="_button" :class="$style.button">
|
||||
<template v-if="showBody"><i class="ti ti-chevron-up"></i></template>
|
||||
<template v-else><i class="ti ti-chevron-down"></i></template>
|
||||
</button>
|
||||
@@ -11,9 +11,9 @@
|
||||
<Transition
|
||||
:name="defaultStore.state.animation ? 'folder-toggle' : ''"
|
||||
@enter="enter"
|
||||
@after-enter="afterEnter"
|
||||
@afterEnter="afterEnter"
|
||||
@leave="leave"
|
||||
@after-leave="afterLeave"
|
||||
@afterLeave="afterLeave"
|
||||
>
|
||||
<div v-show="showBody">
|
||||
<slot></slot>
|
||||
@@ -22,84 +22,71 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref, shallowRef, watch } from 'vue';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { miLocalStorage } from '@/local-storage';
|
||||
import { defaultStore } from '@/store';
|
||||
|
||||
const miLocalStoragePrefix = 'ui:folder:' as const;
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
expanded: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
persistKey: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
defaultStore,
|
||||
bg: null,
|
||||
showBody: (this.persistKey && miLocalStorage.getItem(`${miLocalStoragePrefix}${this.persistKey}`)) ? (miLocalStorage.getItem(`${miLocalStoragePrefix}${this.persistKey}`) === 't') : this.expanded,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
showBody() {
|
||||
if (this.persistKey) {
|
||||
miLocalStorage.setItem(`${miLocalStoragePrefix}${this.persistKey}`, this.showBody ? 't' : 'f');
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
function getParentBg(el: Element | null): string {
|
||||
if (el == null || el.tagName === 'BODY') return 'var(--bg)';
|
||||
const bg = el.style.background || el.style.backgroundColor;
|
||||
if (bg) {
|
||||
return bg;
|
||||
} else {
|
||||
return getParentBg(el.parentElement);
|
||||
}
|
||||
}
|
||||
const rawBg = getParentBg(this.$el);
|
||||
const bg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
|
||||
bg.setAlpha(0.85);
|
||||
this.bg = bg.toRgbString();
|
||||
},
|
||||
methods: {
|
||||
toggleContent(show: boolean) {
|
||||
this.showBody = show;
|
||||
},
|
||||
const props = withDefaults(defineProps<{
|
||||
expanded?: boolean;
|
||||
persistKey?: string;
|
||||
}>(), {
|
||||
expanded: true,
|
||||
});
|
||||
|
||||
enter(el) {
|
||||
const elementHeight = el.getBoundingClientRect().height;
|
||||
el.style.height = 0;
|
||||
el.offsetHeight; // reflow
|
||||
el.style.height = elementHeight + 'px';
|
||||
},
|
||||
afterEnter(el) {
|
||||
el.style.height = null;
|
||||
},
|
||||
leave(el) {
|
||||
const elementHeight = el.getBoundingClientRect().height;
|
||||
el.style.height = elementHeight + 'px';
|
||||
el.offsetHeight; // reflow
|
||||
el.style.height = 0;
|
||||
},
|
||||
afterLeave(el) {
|
||||
el.style.height = null;
|
||||
},
|
||||
},
|
||||
const el = shallowRef<HTMLDivElement>();
|
||||
const bg = ref<string | null>(null);
|
||||
const showBody = ref((props.persistKey && miLocalStorage.getItem(`${miLocalStoragePrefix}${props.persistKey}`)) ? (miLocalStorage.getItem(`${miLocalStoragePrefix}${props.persistKey}`) === 't') : props.expanded);
|
||||
|
||||
watch(showBody, () => {
|
||||
if (props.persistKey) {
|
||||
miLocalStorage.setItem(`${miLocalStoragePrefix}${props.persistKey}`, showBody.value ? 't' : 'f');
|
||||
}
|
||||
});
|
||||
|
||||
function enter(el: Element) {
|
||||
const elementHeight = el.getBoundingClientRect().height;
|
||||
el.style.height = 0;
|
||||
el.offsetHeight; // reflow
|
||||
el.style.height = elementHeight + 'px';
|
||||
}
|
||||
|
||||
function afterEnter(el: Element) {
|
||||
el.style.height = null;
|
||||
}
|
||||
|
||||
function leave(el: Element) {
|
||||
const elementHeight = el.getBoundingClientRect().height;
|
||||
el.style.height = elementHeight + 'px';
|
||||
el.offsetHeight; // reflow
|
||||
el.style.height = 0;
|
||||
}
|
||||
|
||||
function afterLeave(el: Element) {
|
||||
el.style.height = null;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
function getParentBg(el: HTMLElement | null): string {
|
||||
if (el == null || el.tagName === 'BODY') return 'var(--bg)';
|
||||
const bg = el.style.background || el.style.backgroundColor;
|
||||
if (bg) {
|
||||
return bg;
|
||||
} else {
|
||||
return getParentBg(el.parentElement);
|
||||
}
|
||||
}
|
||||
const rawBg = getParentBg(el.value);
|
||||
const _bg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
|
||||
_bg.setAlpha(0.85);
|
||||
bg.value = _bg.toRgbString();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
<style lang="scss" module>
|
||||
.folder-toggle-enter-active, .folder-toggle-leave-active {
|
||||
overflow-y: clip;
|
||||
transition: opacity 0.5s, height 0.5s !important;
|
||||
@@ -111,45 +98,41 @@ export default defineComponent({
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.ssazuxis {
|
||||
.root {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
> header {
|
||||
display: flex;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
position: sticky;
|
||||
top: var(--stickyTop, 0px);
|
||||
-webkit-backdrop-filter: var(--blur, blur(8px));
|
||||
backdrop-filter: var(--blur, blur(20px));
|
||||
.header {
|
||||
display: flex;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
position: sticky;
|
||||
top: var(--stickyTop, 0px);
|
||||
-webkit-backdrop-filter: var(--blur, blur(8px));
|
||||
backdrop-filter: var(--blur, blur(20px));
|
||||
}
|
||||
|
||||
> .title {
|
||||
display: grid;
|
||||
place-content: center;
|
||||
margin: 0;
|
||||
padding: 12px 16px 12px 0;
|
||||
}
|
||||
.title {
|
||||
display: grid;
|
||||
place-content: center;
|
||||
margin: 0;
|
||||
padding: 12px 16px 12px 0;
|
||||
}
|
||||
|
||||
> .divider {
|
||||
flex: 1;
|
||||
margin: auto;
|
||||
height: 1px;
|
||||
background: var(--divider);
|
||||
}
|
||||
.divider {
|
||||
flex: 1;
|
||||
margin: auto;
|
||||
height: 1px;
|
||||
background: var(--divider);
|
||||
}
|
||||
|
||||
> button {
|
||||
padding: 12px 0 12px 16px;
|
||||
}
|
||||
}
|
||||
.button {
|
||||
padding: 12px 0 12px 16px;
|
||||
}
|
||||
|
||||
@container (max-width: 500px) {
|
||||
.ssazuxis {
|
||||
> header {
|
||||
> .title {
|
||||
padding: 8px 10px 8px 0;
|
||||
}
|
||||
}
|
||||
.title {
|
||||
padding: 8px 10px 8px 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -5,8 +5,8 @@
|
||||
<div :class="[$style.header, { [$style.opened]: opened }]" class="_button" role="button" data-cy-folder-header @click="toggle">
|
||||
<div :class="$style.headerIcon"><slot name="icon"></slot></div>
|
||||
<div :class="$style.headerText">
|
||||
<div :class="$style.headerTextMain">
|
||||
<slot name="label"></slot>
|
||||
<div>
|
||||
<MkCondensedLine :minScale="2 / 3"><slot name="label"></slot></MkCondensedLine>
|
||||
</div>
|
||||
<div :class="$style.headerTextSub">
|
||||
<slot name="caption"></slot>
|
||||
@@ -22,18 +22,18 @@
|
||||
|
||||
<div v-if="openedAtLeastOnce" :class="[$style.body, { [$style.bgSame]: bgSame }]" :style="{ maxHeight: maxHeight ? `${maxHeight}px` : null, overflow: maxHeight ? `auto` : null }" :aria-hidden="!opened">
|
||||
<Transition
|
||||
:enter-active-class="defaultStore.state.animation ? $style.transition_toggle_enterActive : ''"
|
||||
:leave-active-class="defaultStore.state.animation ? $style.transition_toggle_leaveActive : ''"
|
||||
:enter-from-class="defaultStore.state.animation ? $style.transition_toggle_enterFrom : ''"
|
||||
:leave-to-class="defaultStore.state.animation ? $style.transition_toggle_leaveTo : ''"
|
||||
:enterActiveClass="defaultStore.state.animation ? $style.transition_toggle_enterActive : ''"
|
||||
:leaveActiveClass="defaultStore.state.animation ? $style.transition_toggle_leaveActive : ''"
|
||||
:enterFromClass="defaultStore.state.animation ? $style.transition_toggle_enterFrom : ''"
|
||||
:leaveToClass="defaultStore.state.animation ? $style.transition_toggle_leaveTo : ''"
|
||||
@enter="enter"
|
||||
@after-enter="afterEnter"
|
||||
@afterEnter="afterEnter"
|
||||
@leave="leave"
|
||||
@after-leave="afterLeave"
|
||||
@afterLeave="afterLeave"
|
||||
>
|
||||
<KeepAlive>
|
||||
<div v-show="opened">
|
||||
<MkSpacer :margin-min="14" :margin-max="22">
|
||||
<MkSpacer :marginMin="14" :marginMax="22">
|
||||
<slot></slot>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
@@ -185,10 +185,6 @@ onMounted(() => {
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
||||
.headerTextMain {
|
||||
|
||||
}
|
||||
|
||||
.headerTextSub {
|
||||
color: var(--fgTransparentWeak);
|
||||
font-size: .85em;
|
||||
|
@@ -1,30 +1,30 @@
|
||||
<template>
|
||||
<button
|
||||
class="kpoogebi _button"
|
||||
:class="{ wait, active: isFollowing || hasPendingFollowRequestFromYou, full, large }"
|
||||
class="_button"
|
||||
:class="[$style.root, { [$style.wait]: wait, [$style.active]: isFollowing || hasPendingFollowRequestFromYou, [$style.full]: full, [$style.large]: large }]"
|
||||
:disabled="wait"
|
||||
@click="onClick"
|
||||
>
|
||||
<template v-if="!wait">
|
||||
<template v-if="hasPendingFollowRequestFromYou && user.isLocked">
|
||||
<span v-if="full">{{ i18n.ts.followRequestPending }}</span><i class="ti ti-hourglass-empty"></i>
|
||||
<span v-if="full" :class="$style.text">{{ i18n.ts.followRequestPending }}</span><i class="ti ti-hourglass-empty"></i>
|
||||
</template>
|
||||
<template v-else-if="hasPendingFollowRequestFromYou && !user.isLocked">
|
||||
<!-- つまりリモートフォローの場合。 -->
|
||||
<span v-if="full">{{ i18n.ts.processing }}</span><MkLoading :em="true" :colored="false"/>
|
||||
<span v-if="full" :class="$style.text">{{ i18n.ts.processing }}</span><MkLoading :em="true" :colored="false"/>
|
||||
</template>
|
||||
<template v-else-if="isFollowing">
|
||||
<span v-if="full">{{ i18n.ts.unfollow }}</span><i class="ti ti-minus"></i>
|
||||
<span v-if="full" :class="$style.text">{{ i18n.ts.unfollow }}</span><i class="ti ti-minus"></i>
|
||||
</template>
|
||||
<template v-else-if="!isFollowing && user.isLocked">
|
||||
<span v-if="full">{{ i18n.ts.followRequest }}</span><i class="ti ti-plus"></i>
|
||||
<span v-if="full" :class="$style.text">{{ i18n.ts.followRequest }}</span><i class="ti ti-plus"></i>
|
||||
</template>
|
||||
<template v-else-if="!isFollowing && !user.isLocked">
|
||||
<span v-if="full">{{ i18n.ts.follow }}</span><i class="ti ti-plus"></i>
|
||||
<span v-if="full" :class="$style.text">{{ i18n.ts.follow }}</span><i class="ti ti-plus"></i>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span v-if="full">{{ i18n.ts.processing }}</span><MkLoading :em="true" :colored="false"/>
|
||||
<span v-if="full" :class="$style.text">{{ i18n.ts.processing }}</span><MkLoading :em="true" :colored="false"/>
|
||||
</template>
|
||||
</button>
|
||||
</template>
|
||||
@@ -33,7 +33,7 @@
|
||||
import { onBeforeUnmount, onMounted } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import * as os from '@/os';
|
||||
import { stream } from '@/stream';
|
||||
import { useStream } from '@/stream';
|
||||
import { i18n } from '@/i18n';
|
||||
import { claimAchievement } from '@/scripts/achievements';
|
||||
import { $i } from '@/account';
|
||||
@@ -50,7 +50,7 @@ const props = withDefaults(defineProps<{
|
||||
let isFollowing = $ref(props.user.isFollowing);
|
||||
let hasPendingFollowRequestFromYou = $ref(props.user.hasPendingFollowRequestFromYou);
|
||||
let wait = $ref(false);
|
||||
const connection = stream.useChannel('main');
|
||||
const connection = useStream().useChannel('main');
|
||||
|
||||
if (props.user.isFollowing == null) {
|
||||
os.api('users/show', {
|
||||
@@ -126,13 +126,12 @@ onBeforeUnmount(() => {
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.kpoogebi {
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
color: var(--accent);
|
||||
background: transparent;
|
||||
color: var(--fgOnWhite);
|
||||
border: solid 1px var(--accent);
|
||||
padding: 0;
|
||||
height: 31px;
|
||||
@@ -196,9 +195,9 @@ onBeforeUnmount(() => {
|
||||
cursor: wait !important;
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
> span {
|
||||
margin-right: 6px;
|
||||
}
|
||||
.text {
|
||||
margin-right: 6px;
|
||||
}
|
||||
</style>
|
||||
|
@@ -8,27 +8,28 @@
|
||||
>
|
||||
<template #header>{{ i18n.ts.forgotPassword }}</template>
|
||||
|
||||
<form v-if="instance.enableEmail" class="bafeceda" @submit.prevent="onSubmit">
|
||||
<div class="main _gaps_m">
|
||||
<MkInput v-model="username" type="text" pattern="^[a-zA-Z0-9_]+$" :spellcheck="false" autofocus required>
|
||||
<template #label>{{ i18n.ts.username }}</template>
|
||||
<template #prefix>@</template>
|
||||
</MkInput>
|
||||
<MkSpacer :marginMin="20" :marginMax="28">
|
||||
<form v-if="instance.enableEmail" @submit.prevent="onSubmit">
|
||||
<div class="_gaps_m">
|
||||
<MkInput v-model="username" type="text" pattern="^[a-zA-Z0-9_]+$" :spellcheck="false" autofocus required>
|
||||
<template #label>{{ i18n.ts.username }}</template>
|
||||
<template #prefix>@</template>
|
||||
</MkInput>
|
||||
|
||||
<MkInput v-model="email" type="email" :spellcheck="false" required>
|
||||
<template #label>{{ i18n.ts.emailAddress }}</template>
|
||||
<template #caption>{{ i18n.ts._forgotPassword.enterEmail }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="email" type="email" :spellcheck="false" required>
|
||||
<template #label>{{ i18n.ts.emailAddress }}</template>
|
||||
<template #caption>{{ i18n.ts._forgotPassword.enterEmail }}</template>
|
||||
</MkInput>
|
||||
|
||||
<MkButton type="submit" :disabled="processing" primary style="margin: 0 auto;">{{ i18n.ts.send }}</MkButton>
|
||||
<MkButton type="submit" rounded :disabled="processing" primary style="margin: 0 auto;">{{ i18n.ts.send }}</MkButton>
|
||||
|
||||
<MkInfo>{{ i18n.ts._forgotPassword.ifNoEmail }}</MkInfo>
|
||||
</div>
|
||||
</form>
|
||||
<div v-else>
|
||||
{{ i18n.ts._forgotPassword.contactAdmin }}
|
||||
</div>
|
||||
<div class="sub">
|
||||
<MkA to="/about" class="_link">{{ i18n.ts._forgotPassword.ifNoEmail }}</MkA>
|
||||
</div>
|
||||
</form>
|
||||
<div v-else class="bafecedb">
|
||||
{{ i18n.ts._forgotPassword.contactAdmin }}
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkModalWindow>
|
||||
</template>
|
||||
|
||||
@@ -37,6 +38,7 @@ import { } from 'vue';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import * as os from '@/os';
|
||||
import { instance } from '@/instance';
|
||||
import { i18n } from '@/i18n';
|
||||
@@ -62,20 +64,3 @@ async function onSubmit() {
|
||||
dialog.close();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.bafeceda {
|
||||
> .main {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
> .sub {
|
||||
border-top: solid 0.5px var(--divider);
|
||||
padding: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.bafecedb {
|
||||
padding: 24px;
|
||||
}
|
||||
</style>
|
||||
|
@@ -2,9 +2,9 @@
|
||||
<MkModalWindow
|
||||
ref="dialog"
|
||||
:width="450"
|
||||
:can-close="false"
|
||||
:with-ok-button="true"
|
||||
:ok-button-disabled="false"
|
||||
:canClose="false"
|
||||
:withOkButton="true"
|
||||
:okButtonDisabled="false"
|
||||
@click="cancel()"
|
||||
@ok="ok()"
|
||||
@close="cancel()"
|
||||
@@ -14,7 +14,7 @@
|
||||
{{ title }}
|
||||
</template>
|
||||
|
||||
<MkSpacer :margin-min="20" :margin-max="32">
|
||||
<MkSpacer :marginMin="20" :marginMax="32">
|
||||
<div class="_gaps_m">
|
||||
<template v-for="item in Object.keys(form).filter(item => !form[item].hidden)">
|
||||
<MkInput v-if="form[item].type === 'number'" v-model="values[item]" type="number" :step="form[item].step || 1">
|
||||
@@ -41,7 +41,7 @@
|
||||
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ i18n.ts.optional }})</span></template>
|
||||
<option v-for="item in form[item].options" :key="item.value" :value="item.value">{{ item.label }}</option>
|
||||
</MkRadios>
|
||||
<MkRange v-else-if="form[item].type === 'range'" v-model="values[item]" :min="form[item].min" :max="form[item].max" :step="form[item].step" :text-converter="form[item].textConverter">
|
||||
<MkRange v-else-if="form[item].type === 'range'" v-model="values[item]" :min="form[item].min" :max="form[item].max" :step="form[item].step" :textConverter="form[item].textConverter">
|
||||
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ i18n.ts.optional }})</span></template>
|
||||
<template v-if="form[item].description" #caption>{{ form[item].description }}</template>
|
||||
</MkRange>
|
||||
@@ -54,8 +54,8 @@
|
||||
</MkModalWindow>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { reactive, shallowRef } from 'vue';
|
||||
import MkInput from './MkInput.vue';
|
||||
import MkTextarea from './MkTextarea.vue';
|
||||
import MkSwitch from './MkSwitch.vue';
|
||||
@@ -66,58 +66,36 @@ import MkRadios from './MkRadios.vue';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkModalWindow,
|
||||
MkInput,
|
||||
MkTextarea,
|
||||
MkSwitch,
|
||||
MkSelect,
|
||||
MkRange,
|
||||
MkButton,
|
||||
MkRadios,
|
||||
},
|
||||
const props = defineProps<{
|
||||
title: string;
|
||||
form: any;
|
||||
}>();
|
||||
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
form: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
const emit = defineEmits<{
|
||||
(ev: 'done', v: {
|
||||
canceled?: boolean;
|
||||
result?: any;
|
||||
}): void;
|
||||
}>();
|
||||
|
||||
emits: ['done'],
|
||||
const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
|
||||
const values = reactive({});
|
||||
|
||||
data() {
|
||||
return {
|
||||
values: {},
|
||||
i18n,
|
||||
};
|
||||
},
|
||||
for (const item in props.form) {
|
||||
values[item] = props.form[item].default ?? null;
|
||||
}
|
||||
|
||||
created() {
|
||||
for (const item in this.form) {
|
||||
this.values[item] = this.form[item].default ?? null;
|
||||
}
|
||||
},
|
||||
function ok() {
|
||||
emit('done', {
|
||||
result: values,
|
||||
});
|
||||
dialog.value.close();
|
||||
}
|
||||
|
||||
methods: {
|
||||
ok() {
|
||||
this.$emit('done', {
|
||||
result: this.values,
|
||||
});
|
||||
this.$refs.dialog.close();
|
||||
},
|
||||
|
||||
cancel() {
|
||||
this.$emit('done', {
|
||||
canceled: true,
|
||||
});
|
||||
this.$refs.dialog.close();
|
||||
},
|
||||
},
|
||||
});
|
||||
function cancel() {
|
||||
emit('done', {
|
||||
canceled: true,
|
||||
});
|
||||
dialog.value.close();
|
||||
}
|
||||
</script>
|
||||
|
@@ -44,6 +44,10 @@ export const Default = {
|
||||
],
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
chromatic: {
|
||||
// FIXME: flaky
|
||||
disableSnapshot: true,
|
||||
},
|
||||
},
|
||||
} satisfies StoryObj<typeof MkGalleryPostPreview>;
|
||||
export const Hover = {
|
||||
|
@@ -5,16 +5,13 @@
|
||||
<ImgWithBlurhash
|
||||
class="img layered"
|
||||
:transition="safe ? null : {
|
||||
enterActiveClass: $style.transition_toggle_enterActive,
|
||||
duration: 500,
|
||||
leaveActiveClass: $style.transition_toggle_leaveActive,
|
||||
enterFromClass: $style.transition_toggle_enterFrom,
|
||||
leaveToClass: $style.transition_toggle_leaveTo,
|
||||
enterToClass: $style.transition_toggle_enterTo,
|
||||
leaveFromClass: $style.transition_toggle_leaveFrom,
|
||||
}"
|
||||
:src="post.files[0].thumbnailUrl"
|
||||
:hash="post.files[0].blurhash"
|
||||
:force-blurhash="!show"
|
||||
:forceBlurhash="!show"
|
||||
/>
|
||||
</Transition>
|
||||
</div>
|
||||
@@ -53,24 +50,16 @@ function leaveHover(): void {
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.transition_toggle_enterActive,
|
||||
.transition_toggle_leaveActive {
|
||||
transition: opacity 0.5s;
|
||||
transition: opacity .5s;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.transition_toggle_enterFrom,
|
||||
.transition_toggle_leaveTo {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.transition_toggle_enterTo,
|
||||
.transition_toggle_leaveFrom {
|
||||
transition: none;
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@@ -1,78 +0,0 @@
|
||||
<template>
|
||||
<MkModal ref="modal" :z-priority="'middle'" @click="modal.close()" @closed="emit('closed')">
|
||||
<div class="xubzgfga">
|
||||
<header>{{ image.name }}</header>
|
||||
<img :src="image.url" :alt="image.comment" :title="image.comment" @click="modal.close()"/>
|
||||
<footer>
|
||||
<span>{{ image.type }}</span>
|
||||
<span>{{ bytes(image.size) }}</span>
|
||||
<span v-if="image.properties && image.properties.width">{{ number(image.properties.width) }}px × {{ number(image.properties.height) }}px</span>
|
||||
</footer>
|
||||
</div>
|
||||
</MkModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import * as misskey from 'misskey-js';
|
||||
import bytes from '@/filters/bytes';
|
||||
import number from '@/filters/number';
|
||||
import MkModal from '@/components/MkModal.vue';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
image: misskey.entities.DriveFile;
|
||||
}>(), {
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'closed'): void;
|
||||
}>();
|
||||
|
||||
const modal = $shallowRef<InstanceType<typeof MkModal>>();
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.xubzgfga {
|
||||
margin: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
> header,
|
||||
> footer {
|
||||
align-self: center;
|
||||
display: inline-block;
|
||||
padding: 6px 9px;
|
||||
font-size: 90%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
border-radius: 6px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
> header {
|
||||
margin-bottom: 8px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
> img {
|
||||
display: block;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
object-fit: contain;
|
||||
width: 100%;
|
||||
cursor: zoom-out;
|
||||
image-orientation: from-image;
|
||||
}
|
||||
|
||||
> footer {
|
||||
margin-top: 8px;
|
||||
opacity: 0.8;
|
||||
|
||||
> span + span {
|
||||
margin-left: 0.5em;
|
||||
padding-left: 0.5em;
|
||||
border-left: solid 1px rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -1,30 +1,60 @@
|
||||
<template>
|
||||
<div :class="[$style.root, { [$style.cover]: cover }]" :title="title ?? ''">
|
||||
<img v-if="!loaded && src && !forceBlurhash" :class="$style.loader" :src="src" @load="onLoad"/>
|
||||
<Transition
|
||||
mode="in-out"
|
||||
:enter-active-class="defaultStore.state.animation && (props.transition?.enterActiveClass ?? $style['transition_toggle_enterActive']) || undefined"
|
||||
:leave-active-class="defaultStore.state.animation && (props.transition?.leaveActiveClass ?? $style['transition_toggle_leaveActive']) || undefined"
|
||||
:enter-from-class="defaultStore.state.animation && props.transition?.enterFromClass || undefined"
|
||||
:leave-to-class="defaultStore.state.animation && props.transition?.leaveToClass || undefined"
|
||||
:enter-to-class="defaultStore.state.animation && (props.transition?.enterToClass ?? $style['transition_toggle_enterTo']) || undefined"
|
||||
:leave-from-class="defaultStore.state.animation && (props.transition?.leaveFromClass ?? $style['transition_toggle_leaveFrom']) || undefined"
|
||||
<div ref="root" :class="['chromatic-ignore', $style.root, { [$style.cover]: cover }]" :title="title ?? ''">
|
||||
<TransitionGroup
|
||||
:duration="defaultStore.state.animation && props.transition?.duration || undefined"
|
||||
:enterActiveClass="defaultStore.state.animation && props.transition?.enterActiveClass || undefined"
|
||||
:leaveActiveClass="defaultStore.state.animation && (props.transition?.leaveActiveClass ?? $style.transition_leaveActive) || undefined"
|
||||
:enterFromClass="defaultStore.state.animation && props.transition?.enterFromClass || undefined"
|
||||
:leaveToClass="defaultStore.state.animation && props.transition?.leaveToClass || undefined"
|
||||
:enterToClass="defaultStore.state.animation && props.transition?.enterToClass || undefined"
|
||||
:leaveFromClass="defaultStore.state.animation && props.transition?.leaveFromClass || undefined"
|
||||
>
|
||||
<canvas v-if="!loaded || forceBlurhash" ref="canvas" :class="$style.canvas" :width="width" :height="height" :title="title ?? undefined"/>
|
||||
<img v-else :class="$style.img" :src="src ?? undefined" :title="title ?? undefined" :alt="alt ?? undefined"/>
|
||||
</Transition>
|
||||
<canvas v-show="hide" key="canvas" ref="canvas" :class="$style.canvas" :width="canvasWidth" :height="canvasHeight" :title="title ?? undefined"/>
|
||||
<img v-show="!hide" key="img" ref="img" :height="imgHeight" :width="imgWidth" :class="$style.img" :src="src ?? undefined" :title="title ?? undefined" :alt="alt ?? undefined" loading="eager" decoding="async"/>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, shallowRef, useCssModule, watch } from 'vue';
|
||||
import { decode } from 'blurhash';
|
||||
import { defaultStore } from '@/store';
|
||||
<script lang="ts">
|
||||
import { $ref } from 'vue/macros';
|
||||
import DrawBlurhash from '@/workers/draw-blurhash?worker';
|
||||
import TestWebGL2 from '@/workers/test-webgl2?worker';
|
||||
import { WorkerMultiDispatch } from '@/scripts/worker-multi-dispatch';
|
||||
import { extractAvgColorFromBlurhash } from '@/scripts/extract-avg-color-from-blurhash';
|
||||
|
||||
const $style = useCssModule();
|
||||
const workerPromise = new Promise<WorkerMultiDispatch | null>(resolve => {
|
||||
// テスト環境で Web Worker インスタンスは作成できない
|
||||
if (import.meta.env.MODE === 'test') {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
const testWorker = new TestWebGL2();
|
||||
testWorker.addEventListener('message', event => {
|
||||
if (event.data.result) {
|
||||
const workers = new WorkerMultiDispatch(
|
||||
() => new DrawBlurhash(),
|
||||
Math.min(navigator.hardwareConcurrency - 1, 4),
|
||||
);
|
||||
resolve(workers);
|
||||
if (_DEV_) console.log('WebGL2 in worker is supported!');
|
||||
} else {
|
||||
resolve(null);
|
||||
if (_DEV_) console.log('WebGL2 in worker is not supported...');
|
||||
}
|
||||
testWorker.terminate();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, nextTick, onMounted, onUnmounted, shallowRef, watch } from 'vue';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { render } from 'buraha';
|
||||
import { defaultStore } from '@/store';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
transition?: {
|
||||
duration?: number | { enter: number; leave: number; };
|
||||
enterActiveClass?: string;
|
||||
leaveActiveClass?: string;
|
||||
enterFromClass?: string;
|
||||
@@ -51,67 +81,141 @@ const props = withDefaults(defineProps<{
|
||||
forceBlurhash: false,
|
||||
});
|
||||
|
||||
const viewId = uuid();
|
||||
const canvas = shallowRef<HTMLCanvasElement>();
|
||||
const root = shallowRef<HTMLDivElement>();
|
||||
const img = shallowRef<HTMLImageElement>();
|
||||
let loaded = $ref(false);
|
||||
let width = $ref(props.width);
|
||||
let height = $ref(props.height);
|
||||
let canvasWidth = $ref(64);
|
||||
let canvasHeight = $ref(64);
|
||||
let imgWidth = $ref(props.width);
|
||||
let imgHeight = $ref(props.height);
|
||||
let bitmapTmp = $ref<CanvasImageSource | undefined>();
|
||||
const hide = computed(() => !loaded || props.forceBlurhash);
|
||||
|
||||
function onLoad() {
|
||||
loaded = true;
|
||||
function waitForDecode() {
|
||||
if (props.src != null && props.src !== '') {
|
||||
nextTick()
|
||||
.then(() => img.value?.decode())
|
||||
.then(() => {
|
||||
loaded = true;
|
||||
}, error => {
|
||||
console.error('Error occured during decoding image', img.value, error);
|
||||
throw Error(error);
|
||||
});
|
||||
} else {
|
||||
loaded = false;
|
||||
}
|
||||
}
|
||||
|
||||
watch([() => props.width, () => props.height], () => {
|
||||
watch([() => props.width, () => props.height, root], () => {
|
||||
const ratio = props.width / props.height;
|
||||
if (ratio > 1) {
|
||||
width = Math.round(64 * ratio);
|
||||
height = 64;
|
||||
canvasWidth = Math.round(64 * ratio);
|
||||
canvasHeight = 64;
|
||||
} else {
|
||||
width = 64;
|
||||
height = Math.round(64 / ratio);
|
||||
canvasWidth = 64;
|
||||
canvasHeight = Math.round(64 / ratio);
|
||||
}
|
||||
|
||||
const clientWidth = root.value?.clientWidth ?? 300;
|
||||
imgWidth = clientWidth;
|
||||
imgHeight = Math.round(clientWidth / ratio);
|
||||
}, {
|
||||
immediate: true,
|
||||
});
|
||||
|
||||
function draw() {
|
||||
if (props.hash == null || !canvas.value) return;
|
||||
const pixels = decode(props.hash, width, height);
|
||||
function drawImage(bitmap: CanvasImageSource) {
|
||||
// canvasがない(mountedされていない)場合はTmpに保存しておく
|
||||
if (!canvas.value) {
|
||||
bitmapTmp = bitmap;
|
||||
return;
|
||||
}
|
||||
|
||||
// canvasがあれば描画する
|
||||
bitmapTmp = undefined;
|
||||
const ctx = canvas.value.getContext('2d');
|
||||
const imageData = ctx!.createImageData(width, height);
|
||||
imageData.data.set(pixels);
|
||||
ctx!.putImageData(imageData, 0, 0);
|
||||
if (!ctx) return;
|
||||
ctx.drawImage(bitmap, 0, 0, canvasWidth, canvasHeight);
|
||||
}
|
||||
|
||||
watch([() => props.hash, canvas], () => {
|
||||
async function draw() {
|
||||
if (!canvas.value || props.hash == null) return;
|
||||
|
||||
const ctx = canvas.value.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
// avgColorでお茶をにごす
|
||||
ctx.beginPath();
|
||||
ctx.fillStyle = extractAvgColorFromBlurhash(props.hash) ?? '#888';
|
||||
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
|
||||
|
||||
const workers = await workerPromise;
|
||||
if (workers) {
|
||||
workers.postMessage(
|
||||
{
|
||||
id: viewId,
|
||||
hash: props.hash,
|
||||
width: canvasWidth,
|
||||
height: canvasHeight,
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
} else {
|
||||
try {
|
||||
const work = document.createElement('canvas');
|
||||
work.width = canvasWidth;
|
||||
work.height = canvasHeight;
|
||||
render(props.hash, work);
|
||||
ctx.drawImage(work, 0, 0, canvasWidth, canvasHeight);
|
||||
} catch (error) {
|
||||
console.error('Error occured during drawing blurhash', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function workerOnMessage(event: MessageEvent) {
|
||||
if (event.data.id !== viewId) return;
|
||||
drawImage(event.data.bitmap as ImageBitmap);
|
||||
}
|
||||
|
||||
workerPromise.then(worker => {
|
||||
if (worker) {
|
||||
worker.addListener(workerOnMessage);
|
||||
}
|
||||
|
||||
draw();
|
||||
});
|
||||
|
||||
watch(() => props.src, () => {
|
||||
waitForDecode();
|
||||
});
|
||||
|
||||
watch(() => props.hash, () => {
|
||||
draw();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
draw();
|
||||
// drawImageがmountedより先に呼ばれている場合はここで描画する
|
||||
if (bitmapTmp) {
|
||||
drawImage(bitmapTmp);
|
||||
}
|
||||
waitForDecode();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
workerPromise.then(worker => {
|
||||
worker?.removeListener(workerOnMessage);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.transition_toggle_enterActive,
|
||||
.transition_toggle_leaveActive {
|
||||
.transition_leaveActive {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.transition_toggle_enterTo,
|
||||
.transition_toggle_leaveFrom {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.loader {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.root {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<div class="alqyeyti" :class="{ oneline }">
|
||||
<div class="key">
|
||||
<div :class="[$style.root, { [$style.oneline]: oneline }]">
|
||||
<div :class="$style.key">
|
||||
<slot name="key"></slot>
|
||||
</div>
|
||||
<div class="value">
|
||||
<div :class="$style.value">
|
||||
<slot name="value"></slot>
|
||||
<button v-if="copy" v-tooltip="i18n.ts.copy" class="_textButton" style="margin-left: 0.5em;" @click="copy_"><i class="ti ti-copy"></i></button>
|
||||
</div>
|
||||
@@ -30,24 +30,18 @@ const copy_ = () => {
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.alqyeyti {
|
||||
> .key {
|
||||
font-size: 0.85em;
|
||||
padding: 0 0 0.25em 0;
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
&.oneline {
|
||||
display: flex;
|
||||
|
||||
> .key {
|
||||
.key {
|
||||
width: 30%;
|
||||
font-size: 1em;
|
||||
padding: 0 8px 0 0;
|
||||
}
|
||||
|
||||
> .value {
|
||||
.value {
|
||||
width: 70%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
@@ -55,4 +49,10 @@ const copy_ = () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.key {
|
||||
font-size: 0.85em;
|
||||
padding: 0 0 0.25em 0;
|
||||
opacity: 0.75;
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<MkModal ref="modal" v-slot="{ type, maxHeight }" :prefer-type="preferedModalType" :anchor="anchor" :transparent-bg="true" :src="src" @click="modal.close()" @closed="emit('closed')">
|
||||
<MkModal ref="modal" v-slot="{ type, maxHeight }" :preferType="preferedModalType" :anchor="anchor" :transparentBg="true" :src="src" @click="modal.close()" @closed="emit('closed')">
|
||||
<div class="szkkfdyq _popup _shadow" :class="{ asDrawer: type === 'drawer' }" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : '' }">
|
||||
<div class="main">
|
||||
<template v-for="item in items">
|
||||
|
@@ -1,27 +1,27 @@
|
||||
<template>
|
||||
<div class="mk-media-banner">
|
||||
<div v-if="media.isSensitive && hide" class="sensitive" @click="hide = false">
|
||||
<span class="icon"><i class="ti ti-alert-triangle"></i></span>
|
||||
<div :class="$style.root">
|
||||
<div v-if="media.isSensitive && hide" :class="$style.sensitive" @click="hide = false">
|
||||
<span style="font-size: 1.6em;"><i class="ti ti-alert-triangle"></i></span>
|
||||
<b>{{ i18n.ts.sensitive }}</b>
|
||||
<span>{{ i18n.ts.clickToShow }}</span>
|
||||
</div>
|
||||
<div v-else-if="media.type.startsWith('audio') && media.type !== 'audio/midi'" class="audio">
|
||||
<VuePlyr :options="{ volume: 0.5 }">
|
||||
<audio controls preload="metadata">
|
||||
<source
|
||||
:src="media.url"
|
||||
:type="media.type"
|
||||
/>
|
||||
</audio>
|
||||
</VuePlyr>
|
||||
<div v-else-if="media.type.startsWith('audio') && media.type !== 'audio/midi'" :class="$style.audio">
|
||||
<audio
|
||||
ref="audioEl"
|
||||
:src="media.url"
|
||||
:title="media.name"
|
||||
controls
|
||||
preload="metadata"
|
||||
@volumechange="volumechange"
|
||||
/>
|
||||
</div>
|
||||
<a
|
||||
v-else class="download"
|
||||
v-else :class="$style.download"
|
||||
:href="media.url"
|
||||
:title="media.name"
|
||||
:download="media.name"
|
||||
>
|
||||
<span class="icon"><i class="ti ti-download"></i></span>
|
||||
<span style="font-size: 1.6em;"><i class="ti ti-download"></i></span>
|
||||
<b>{{ media.name }}</b>
|
||||
</a>
|
||||
</div>
|
||||
@@ -30,9 +30,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted } from 'vue';
|
||||
import * as misskey from 'misskey-js';
|
||||
import VuePlyr from 'vue-plyr';
|
||||
import { soundConfigStore } from '@/scripts/sound';
|
||||
import 'vue-plyr/dist/vue-plyr.css';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
@@ -52,55 +50,34 @@ onMounted(() => {
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.mk-media-banner {
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
width: 100%;
|
||||
border-radius: 4px;
|
||||
margin-top: 4px;
|
||||
// overflow: clip;
|
||||
overflow: clip;
|
||||
}
|
||||
|
||||
--plyr-color-main: var(--accent);
|
||||
--plyr-audio-controls-background: var(--bg);
|
||||
--plyr-audio-controls-color: var(--accentLighten);
|
||||
.download,
|
||||
.sensitive {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
padding: 8px 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
> .download,
|
||||
> .sensitive {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
padding: 8px 12px;
|
||||
white-space: nowrap;
|
||||
.download {
|
||||
background: var(--noteAttachedFile);
|
||||
}
|
||||
|
||||
> * {
|
||||
display: block;
|
||||
}
|
||||
.sensitive {
|
||||
background: #111;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
> b {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
> *:not(:last-child) {
|
||||
margin-right: .2em;
|
||||
}
|
||||
|
||||
> .icon {
|
||||
font-size: 1.6em;
|
||||
}
|
||||
}
|
||||
|
||||
> .download {
|
||||
background: var(--noteAttachedFile);
|
||||
}
|
||||
|
||||
> .sensitive {
|
||||
background: #111;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
> .audio {
|
||||
border-radius: 8px;
|
||||
// overflow: clip;
|
||||
}
|
||||
.audio {
|
||||
border-radius: 8px;
|
||||
overflow: clip;
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,29 +1,39 @@
|
||||
<template>
|
||||
<div v-if="hide" :class="$style.hidden" @click="hide = false">
|
||||
<ImgWithBlurhash style="filter: brightness(0.5);" :hash="image.blurhash" :title="image.comment" :alt="image.comment" :width="image.properties.width" :height="image.properties.height" :force-blurhash="defaultStore.state.enableDataSaverMode"/>
|
||||
<div :class="$style.hiddenText">
|
||||
<div :class="$style.hiddenTextWrapper">
|
||||
<b v-if="image.isSensitive" style="display: block;"><i class="ti ti-alert-triangle"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.enableDataSaverMode ? ` (${i18n.ts.image}${image.size ? ' ' + bytes(image.size) : ''})` : '' }}</b>
|
||||
<b v-else style="display: block;"><i class="ti ti-photo"></i> {{ defaultStore.state.enableDataSaverMode && image.size ? bytes(image.size) : i18n.ts.image }}</b>
|
||||
<span style="display: block;">{{ i18n.ts.clickToShow }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else :class="$style.visible" :style="darkMode ? '--c: rgb(255 255 255 / 2%);' : '--c: rgb(0 0 0 / 2%);'">
|
||||
<div :class="hide ? $style.hidden : $style.visible" :style="darkMode ? '--c: rgb(255 255 255 / 2%);' : '--c: rgb(0 0 0 / 2%);'" @click="onclick">
|
||||
<a
|
||||
:class="$style.imageContainer"
|
||||
:href="image.url"
|
||||
:title="image.name"
|
||||
>
|
||||
<ImgWithBlurhash :hash="image.blurhash" :src="url" :alt="image.comment || image.name" :title="image.comment || image.name" :width="image.properties.width" :height="image.properties.height" :cover="false"/>
|
||||
<ImgWithBlurhash
|
||||
:hash="image.blurhash"
|
||||
:src="(defaultStore.state.enableDataSaverMode && hide) ? null : url"
|
||||
:forceBlurhash="hide"
|
||||
:cover="hide"
|
||||
:alt="image.comment || image.name"
|
||||
:title="image.comment || image.name"
|
||||
:width="image.properties.width"
|
||||
:height="image.properties.height"
|
||||
:style="hide ? 'filter: brightness(0.5);' : null"
|
||||
/>
|
||||
</a>
|
||||
<div :class="$style.indicators">
|
||||
<div v-if="['image/gif', 'image/apng'].includes(image.type)" :class="$style.indicator">GIF</div>
|
||||
<div v-if="image.comment" :class="$style.indicator">ALT</div>
|
||||
<div v-if="image.isSensitive" :class="$style.indicator" style="color: var(--warn);">NSFW</div>
|
||||
</div>
|
||||
<button v-tooltip="i18n.ts.hide" :class="$style.hide" class="_button" @click="hide = true"><i class="ti ti-eye-off"></i></button>
|
||||
<button :class="$style.menu" class="_button" @click.stop="showMenu"><i class="ti ti-dots"></i></button>
|
||||
<template v-if="hide">
|
||||
<div :class="$style.hiddenText">
|
||||
<div :class="$style.hiddenTextWrapper">
|
||||
<b v-if="image.isSensitive" style="display: block;"><i class="ti ti-alert-triangle"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.enableDataSaverMode ? ` (${i18n.ts.image}${image.size ? ' ' + bytes(image.size) : ''})` : '' }}</b>
|
||||
<b v-else style="display: block;"><i class="ti ti-photo"></i> {{ defaultStore.state.enableDataSaverMode && image.size ? bytes(image.size) : i18n.ts.image }}</b>
|
||||
<span style="display: block;">{{ i18n.ts.clickToShow }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div :class="$style.indicators">
|
||||
<div v-if="['image/gif', 'image/apng'].includes(image.type)" :class="$style.indicator">GIF</div>
|
||||
<div v-if="image.comment" :class="$style.indicator">ALT</div>
|
||||
<div v-if="image.isSensitive" :class="$style.indicator" style="color: var(--warn);">NSFW</div>
|
||||
</div>
|
||||
<button :class="$style.menu" class="_button" @click.stop="showMenu"><i class="ti ti-dots" style="vertical-align: middle;"></i></button>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -53,6 +63,12 @@ const url = $computed(() => (props.raw || defaultStore.state.loadRawImages)
|
||||
: props.image.thumbnailUrl,
|
||||
);
|
||||
|
||||
function onclick() {
|
||||
if (hide) {
|
||||
hide = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Plugin:register_note_view_interruptor を使って書き換えられる可能性があるためwatchする
|
||||
watch(() => props.image, () => {
|
||||
hide = (defaultStore.state.nsfw === 'force' || defaultStore.state.enableDataSaverMode) ? true : (props.image.isSensitive && defaultStore.state.nsfw !== 'ignore');
|
||||
@@ -62,9 +78,16 @@ watch(() => props.image, () => {
|
||||
});
|
||||
|
||||
function showMenu(ev: MouseEvent) {
|
||||
os.popupMenu([...(iAmModerator ? [{
|
||||
text: i18n.ts.markAsSensitive,
|
||||
os.popupMenu([{
|
||||
text: i18n.ts.hide,
|
||||
icon: 'ti ti-eye-off',
|
||||
action: () => {
|
||||
hide = true;
|
||||
},
|
||||
}, ...(iAmModerator ? [{
|
||||
text: i18n.ts.markAsSensitive,
|
||||
icon: 'ti ti-eye-exclamation',
|
||||
danger: true,
|
||||
action: () => {
|
||||
os.apiWithDialog('drive/files/update', { fileId: props.image.id, isSensitive: true });
|
||||
},
|
||||
@@ -105,34 +128,20 @@ function showMenu(ev: MouseEvent) {
|
||||
background-size: 16px 16px;
|
||||
}
|
||||
|
||||
.hide {
|
||||
display: block;
|
||||
position: absolute;
|
||||
border-radius: 6px;
|
||||
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;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
}
|
||||
|
||||
.menu {
|
||||
display: block;
|
||||
position: absolute;
|
||||
border-radius: 6px;
|
||||
border-radius: 999px;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
-webkit-backdrop-filter: var(--blur, blur(15px));
|
||||
backdrop-filter: var(--blur, blur(15px));
|
||||
color: #fff;
|
||||
font-size: 0.8em;
|
||||
padding: 6px 8px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
text-align: center;
|
||||
bottom: 12px;
|
||||
right: 12px;
|
||||
bottom: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
.imageContainer {
|
||||
@@ -149,12 +158,10 @@ function showMenu(ev: MouseEvent) {
|
||||
.indicators {
|
||||
display: inline-flex;
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
left: 12px;
|
||||
text-align: center;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
pointer-events: none;
|
||||
opacity: .5;
|
||||
font-size: 14px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
@@ -165,7 +172,7 @@ function showMenu(ev: MouseEvent) {
|
||||
color: var(--accentLighten);
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
padding: 2px 6px;
|
||||
font-size: 0.8em;
|
||||
padding: 2px 5px;
|
||||
}
|
||||
</style>
|
||||
|
@@ -6,7 +6,11 @@
|
||||
ref="gallery"
|
||||
:class="[
|
||||
$style.medias,
|
||||
count <= 4 ? $style['n' + count] : $style.nMany,
|
||||
count === 1 ? [$style.n1, {
|
||||
[$style.n116_9]: defaultStore.reactiveState.mediaListWithOneImageAppearance.value === '16_9',
|
||||
[$style.n11_1]: defaultStore.reactiveState.mediaListWithOneImageAppearance.value === '1_1',
|
||||
[$style.n12_3]: defaultStore.reactiveState.mediaListWithOneImageAppearance.value === '2_3',
|
||||
}] : count === 2 ? $style.n2 : count === 3 ? $style.n3 : count === 4 ? $style.n4 : $style.nMany,
|
||||
]"
|
||||
>
|
||||
<template v-for="media in mediaList.filter(media => previewable(media))">
|
||||
@@ -19,7 +23,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref, useCssModule, watch } from 'vue';
|
||||
import { onMounted, watch, shallowRef } from 'vue';
|
||||
import * as misskey from 'misskey-js';
|
||||
import PhotoSwipeLightbox from 'photoswipe/lightbox';
|
||||
import PhotoSwipe from 'photoswipe';
|
||||
@@ -36,13 +40,42 @@ const props = defineProps<{
|
||||
raw?: boolean;
|
||||
}>();
|
||||
|
||||
const $style = useCssModule();
|
||||
|
||||
const gallery = ref<HTMLDivElement>();
|
||||
const gallery = shallowRef<HTMLDivElement>();
|
||||
const pswpZIndex = os.claimZIndex('middle');
|
||||
document.documentElement.style.setProperty('--mk-pswp-root-z-index', pswpZIndex.toString());
|
||||
const count = $computed(() => props.mediaList.filter(media => previewable(media)).length);
|
||||
|
||||
function calcAspectRatio() {
|
||||
if (!gallery.value) return;
|
||||
|
||||
let img = props.mediaList[0];
|
||||
|
||||
if (props.mediaList.length !== 1 || !(img.properties.width && img.properties.height)) {
|
||||
gallery.value.style.aspectRatio = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// アスペクト比上限設定では、横長の場合は高さを縮小させる
|
||||
const ratioMax = (ratio: number) => `${Math.max(ratio, img.properties.width / img.properties.height).toString()} / 1`;
|
||||
|
||||
switch (defaultStore.state.mediaListWithOneImageAppearance) {
|
||||
case '16_9':
|
||||
gallery.value.style.aspectRatio = ratioMax(16 / 9);
|
||||
break;
|
||||
case '1_1':
|
||||
gallery.value.style.aspectRatio = ratioMax(1);
|
||||
break;
|
||||
case '2_3':
|
||||
gallery.value.style.aspectRatio = ratioMax(2 / 3);
|
||||
break;
|
||||
default:
|
||||
gallery.value.style.aspectRatio = '';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
watch([defaultStore.reactiveState.mediaListWithOneImageAppearance, gallery], () => calcAspectRatio());
|
||||
|
||||
onMounted(() => {
|
||||
const lightbox = new PhotoSwipeLightbox({
|
||||
dataSource: props.mediaList
|
||||
@@ -64,7 +97,7 @@ onMounted(() => {
|
||||
return item;
|
||||
}),
|
||||
gallery: gallery.value,
|
||||
mainClass: $style.pswp,
|
||||
mainClass: 'pswp',
|
||||
children: '.image',
|
||||
thumbSelector: '.image',
|
||||
loop: false,
|
||||
@@ -162,12 +195,37 @@ const previewable = (file: misskey.entities.DriveFile): boolean => {
|
||||
display: grid;
|
||||
grid-gap: 8px;
|
||||
|
||||
// for webkit
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
&.n1 {
|
||||
aspect-ratio: 16/9;
|
||||
grid-template-rows: 1fr;
|
||||
|
||||
// default (expand)
|
||||
min-height: 64px;
|
||||
max-height: clamp(
|
||||
64px,
|
||||
50cqh,
|
||||
min(360px, 50vh)
|
||||
);
|
||||
|
||||
&.n116_9 {
|
||||
min-height: none;
|
||||
max-height: none;
|
||||
aspect-ratio: 16 / 9; // fallback
|
||||
}
|
||||
|
||||
&.n11_1{
|
||||
min-height: none;
|
||||
max-height: none;
|
||||
aspect-ratio: 1 / 1; // fallback
|
||||
}
|
||||
|
||||
&.n12_3 {
|
||||
min-height: none;
|
||||
max-height: none;
|
||||
aspect-ratio: 2 / 3; // fallback
|
||||
}
|
||||
}
|
||||
|
||||
&.n2 {
|
||||
@@ -211,7 +269,7 @@ const previewable = (file: misskey.entities.DriveFile): boolean => {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.pswp {
|
||||
:global(.pswp) {
|
||||
--pswp-root-z-index: var(--mk-pswp-root-z-index, 2000700) !important;
|
||||
--pswp-bg: var(--modalBg) !important;
|
||||
}
|
||||
|
@@ -1,26 +1,28 @@
|
||||
<template>
|
||||
<div v-if="hide" class="icozogqfvdetwohsdglrbswgrejoxbdj" @click="hide = false">
|
||||
<div v-if="hide" :class="$style.hidden" @click="hide = false">
|
||||
<!-- 【注意】dataSaverMode が有効になっている際には、hide が false になるまでサムネイルや動画を読み込まないようにすること -->
|
||||
<div>
|
||||
<b v-if="video.isSensitive"><i class="ti ti-alert-triangle"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.enableDataSaverMode ? ` (${i18n.ts.video}${video.size ? ' ' + bytes(video.size) : ''})` : '' }}</b>
|
||||
<b v-else><i class="ti ti-movie"></i> {{ defaultStore.state.enableDataSaverMode && video.size ? bytes(video.size) : i18n.ts.video }}</b>
|
||||
<div :class="$style.sensitive">
|
||||
<b v-if="video.isSensitive" style="display: block;"><i class="ti ti-alert-triangle"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.enableDataSaverMode ? ` (${i18n.ts.video}${video.size ? ' ' + bytes(video.size) : ''})` : '' }}</b>
|
||||
<b v-else style="display: block;"><i class="ti ti-movie"></i> {{ defaultStore.state.enableDataSaverMode && video.size ? bytes(video.size) : i18n.ts.video }}</b>
|
||||
<span>{{ i18n.ts.clickToShow }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="kkjnbbplepmiyuadieoenjgutgcmtsvu">
|
||||
<VuePlyr :options="{ volume: 0.5 }">
|
||||
<video
|
||||
controls
|
||||
:data-poster="video.thumbnailUrl"
|
||||
<div v-else :class="$style.visible">
|
||||
<video
|
||||
:class="$style.video"
|
||||
:poster="video.thumbnailUrl"
|
||||
:title="video.comment"
|
||||
:alt="video.comment"
|
||||
preload="none"
|
||||
controls
|
||||
@contextmenu.stop
|
||||
>
|
||||
<source
|
||||
:src="video.url"
|
||||
:type="video.type"
|
||||
>
|
||||
<source
|
||||
size="720"
|
||||
:src="video.url"
|
||||
:type="video.type"
|
||||
/>
|
||||
</video>
|
||||
</VuePlyr>
|
||||
<i class="ti ti-eye-off" @click="hide = true"></i>
|
||||
</video>
|
||||
<i class="ti ti-eye-off" :class="$style.hide" @click="hide = true"></i>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -28,9 +30,7 @@
|
||||
import { ref } from 'vue';
|
||||
import * as misskey from 'misskey-js';
|
||||
import bytes from '@/filters/bytes';
|
||||
import VuePlyr from 'vue-plyr';
|
||||
import { defaultStore } from '@/store';
|
||||
import 'vue-plyr/dist/vue-plyr.css';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -40,56 +40,49 @@ const props = defineProps<{
|
||||
const hide = ref((defaultStore.state.nsfw === 'force' || defaultStore.state.enableDataSaverMode) ? true : (props.video.isSensitive && defaultStore.state.nsfw !== 'ignore'));
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.kkjnbbplepmiyuadieoenjgutgcmtsvu {
|
||||
<style lang="scss" module>
|
||||
.visible {
|
||||
position: relative;
|
||||
|
||||
--plyr-color-main: var(--accent);
|
||||
|
||||
> i {
|
||||
display: block;
|
||||
position: absolute;
|
||||
border-radius: 6px;
|
||||
background-color: var(--fg);
|
||||
color: var(--accentLighten);
|
||||
font-size: 14px;
|
||||
opacity: .5;
|
||||
padding: 3px 6px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
}
|
||||
|
||||
> video {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
font-size: 3.5em;
|
||||
overflow: hidden;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.icozogqfvdetwohsdglrbswgrejoxbdj {
|
||||
.hide {
|
||||
display: block;
|
||||
position: absolute;
|
||||
border-radius: 6px;
|
||||
background-color: var(--fg);
|
||||
color: var(--accentLighten);
|
||||
font-size: 14px;
|
||||
opacity: .5;
|
||||
padding: 3px 6px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
}
|
||||
|
||||
.video {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 3.5em;
|
||||
overflow: hidden;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: #111;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
> div {
|
||||
display: table-cell;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
|
||||
> b {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
.sensitive {
|
||||
display: table-cell;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
|
@@ -2,7 +2,7 @@
|
||||
<MkA v-user-preview="canonical" :class="[$style.root, { [$style.isMe]: isMe }]" :to="url" :style="{ background: bgCss }">
|
||||
<img :class="$style.icon" :src="`/avatar/@${username}@${host}`" alt="">
|
||||
<span>
|
||||
<span :class="$style.username">@{{ username }}</span>
|
||||
<span>@{{ username }}</span>
|
||||
<span v-if="(host != localHost) || defaultStore.state.showFullAcct" :class="$style.host">@{{ toUnicode(host) }}</span>
|
||||
</span>
|
||||
</MkA>
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div ref="el" :class="$style.root">
|
||||
<MkMenu :items="items" :align="align" :width="width" :as-drawer="false" @close="onChildClosed"/>
|
||||
<MkMenu :items="items" :align="align" :width="width" :asDrawer="false" @close="onChildClosed"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@@ -49,8 +49,8 @@
|
||||
<span>{{ i18n.ts.none }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="childMenu" :class="$style.child">
|
||||
<XChild ref="child" :items="childMenu" :target-element="childTarget" :root-element="itemsEl" showing @actioned="childActioned"/>
|
||||
<div v-if="childMenu">
|
||||
<XChild ref="child" :items="childMenu" :targetElement="childTarget" :rootElement="itemsEl" showing @actioned="childActioned"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@@ -1,11 +1,31 @@
|
||||
<template>
|
||||
<Transition
|
||||
:name="transitionName"
|
||||
:enter-active-class="$style['transition_' + transitionName + '_enterActive']"
|
||||
:leave-active-class="$style['transition_' + transitionName + '_leaveActive']"
|
||||
:enter-from-class="$style['transition_' + transitionName + '_enterFrom']"
|
||||
:leave-to-class="$style['transition_' + transitionName + '_leaveTo']"
|
||||
:duration="transitionDuration" appear @after-leave="emit('closed')" @enter="emit('opening')" @after-enter="onOpened"
|
||||
:enterActiveClass="normalizeClass({
|
||||
[$style.transition_modalDrawer_enterActive]: transitionName === 'modal-drawer',
|
||||
[$style.transition_modalPopup_enterActive]: transitionName === 'modal-popup',
|
||||
[$style.transition_modal_enterActive]: transitionName === 'modal',
|
||||
[$style.transition_send_enterActive]: transitionName === 'send',
|
||||
})"
|
||||
:leaveActiveClass="normalizeClass({
|
||||
[$style.transition_modalDrawer_leaveActive]: transitionName === 'modal-drawer',
|
||||
[$style.transition_modalPopup_leaveActive]: transitionName === 'modal-popup',
|
||||
[$style.transition_modal_leaveActive]: transitionName === 'modal',
|
||||
[$style.transition_send_leaveActive]: transitionName === 'send',
|
||||
})"
|
||||
:enterFromClass="normalizeClass({
|
||||
[$style.transition_modalDrawer_enterFrom]: transitionName === 'modal-drawer',
|
||||
[$style.transition_modalPopup_enterFrom]: transitionName === 'modal-popup',
|
||||
[$style.transition_modal_enterFrom]: transitionName === 'modal',
|
||||
[$style.transition_send_enterFrom]: transitionName === 'send',
|
||||
})"
|
||||
:leaveToClass="normalizeClass({
|
||||
[$style.transition_modalDrawer_leaveTo]: transitionName === 'modal-drawer',
|
||||
[$style.transition_modalPopup_leaveTo]: transitionName === 'modal-popup',
|
||||
[$style.transition_modal_leaveTo]: transitionName === 'modal',
|
||||
[$style.transition_send_leaveTo]: transitionName === 'send',
|
||||
})"
|
||||
:duration="transitionDuration" appear @afterLeave="emit('closed')" @enter="emit('opening')" @afterEnter="onOpened"
|
||||
>
|
||||
<div v-show="manualShowing != null ? manualShowing : showing" v-hotkey.global="keymap" :class="[$style.root, { [$style.drawer]: type === 'drawer', [$style.dialog]: type === 'dialog', [$style.popup]: type === 'popup' }]" :style="{ zIndex, pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }">
|
||||
<div data-cy-bg :data-cy-transparent="isEnableBgTransparent" class="_modalBg" :class="[$style.bg, { [$style.bgTransparent]: isEnableBgTransparent }]" :style="{ zIndex }" @click="onBgClick" @mousedown="onBgClick" @contextmenu.prevent.stop="() => {}"></div>
|
||||
@@ -17,7 +37,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { nextTick, onMounted, watch, provide } from 'vue';
|
||||
import { nextTick, normalizeClass, onMounted, onUnmounted, provide, watch } from 'vue';
|
||||
import * as os from '@/os';
|
||||
import { isTouchUsing } from '@/scripts/touch';
|
||||
import { defaultStore } from '@/store';
|
||||
@@ -38,7 +58,7 @@ type ModalTypes = 'popup' | 'dialog' | 'drawer';
|
||||
const props = withDefaults(defineProps<{
|
||||
manualShowing?: boolean | null;
|
||||
anchor?: { x: string; y: string; };
|
||||
src?: HTMLElement;
|
||||
src?: HTMLElement | null;
|
||||
preferType?: ModalTypes | 'auto';
|
||||
zPriority?: 'low' | 'middle' | 'high';
|
||||
noOverlap?: boolean;
|
||||
@@ -264,6 +284,10 @@ const onOpened = () => {
|
||||
}, { passive: true });
|
||||
};
|
||||
|
||||
const alignObserver = new ResizeObserver((entries, observer) => {
|
||||
align();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
watch(() => props.src, async () => {
|
||||
if (props.src) {
|
||||
@@ -278,12 +302,14 @@ onMounted(() => {
|
||||
}, { immediate: true });
|
||||
|
||||
nextTick(() => {
|
||||
new ResizeObserver((entries, observer) => {
|
||||
align();
|
||||
}).observe(content!);
|
||||
alignObserver.observe(content!);
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
alignObserver.disconnect();
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
close,
|
||||
});
|
||||
@@ -339,8 +365,8 @@ defineExpose({
|
||||
}
|
||||
}
|
||||
|
||||
.transition_modal-popup_enterActive,
|
||||
.transition_modal-popup_leaveActive {
|
||||
.transition_modalPopup_enterActive,
|
||||
.transition_modalPopup_leaveActive {
|
||||
> .bg {
|
||||
transition: opacity 0.1s !important;
|
||||
}
|
||||
@@ -350,8 +376,8 @@ defineExpose({
|
||||
transition: opacity 0.1s cubic-bezier(0, 0, 0.2, 1), transform 0.1s cubic-bezier(0, 0, 0.2, 1) !important;
|
||||
}
|
||||
}
|
||||
.transition_modal-popup_enterFrom,
|
||||
.transition_modal-popup_leaveTo {
|
||||
.transition_modalPopup_enterFrom,
|
||||
.transition_modalPopup_leaveTo {
|
||||
> .bg {
|
||||
opacity: 0;
|
||||
}
|
||||
@@ -364,7 +390,7 @@ defineExpose({
|
||||
}
|
||||
}
|
||||
|
||||
.transition_modal-drawer_enterActive {
|
||||
.transition_modalDrawer_enterActive {
|
||||
> .bg {
|
||||
transition: opacity 0.2s !important;
|
||||
}
|
||||
@@ -373,7 +399,7 @@ defineExpose({
|
||||
transition: transform 0.2s cubic-bezier(0,.5,0,1) !important;
|
||||
}
|
||||
}
|
||||
.transition_modal-drawer_leaveActive {
|
||||
.transition_modalDrawer_leaveActive {
|
||||
> .bg {
|
||||
transition: opacity 0.2s !important;
|
||||
}
|
||||
@@ -382,8 +408,8 @@ defineExpose({
|
||||
transition: transform 0.2s cubic-bezier(0,.5,0,1) !important;
|
||||
}
|
||||
}
|
||||
.transition_modal-drawer_enterFrom,
|
||||
.transition_modal-drawer_leaveTo {
|
||||
.transition_modalDrawer_enterFrom,
|
||||
.transition_modalDrawer_leaveTo {
|
||||
> .bg {
|
||||
opacity: 0;
|
||||
}
|
||||
|
@@ -1,182 +0,0 @@
|
||||
<template>
|
||||
<MkModal ref="modal" @click="$emit('click')" @closed="$emit('closed')">
|
||||
<div ref="rootEl" class="hrmcaedk" :style="{ width: `${width}px`, height: (height ? `min(${height}px, 100%)` : '100%') }">
|
||||
<div class="header" @contextmenu="onContextmenu">
|
||||
<button v-if="history.length > 0" v-tooltip="i18n.ts.goBack" class="_button" @click="back()"><i class="ti ti-arrow-left"></i></button>
|
||||
<span v-else style="display: inline-block; width: 20px"></span>
|
||||
<span v-if="pageMetadata?.value" class="title">
|
||||
<i v-if="pageMetadata?.value.icon" class="icon" :class="pageMetadata?.value.icon"></i>
|
||||
<span>{{ pageMetadata?.value.title }}</span>
|
||||
</span>
|
||||
<button class="_button" @click="$refs.modal.close()"><i class="ti ti-x"></i></button>
|
||||
</div>
|
||||
<div class="body" style="container-type: inline-size;">
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader v-if="pageMetadata?.value && !pageMetadata?.value.hideHeader" :info="pageMetadata?.value"/></template>
|
||||
<RouterView :router="router"/>
|
||||
</MkStickyContainer>
|
||||
</div>
|
||||
</div>
|
||||
</MkModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ComputedRef, provide } from 'vue';
|
||||
import MkModal from '@/components/MkModal.vue';
|
||||
import { popout as _popout } from '@/scripts/popout';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard';
|
||||
import { url } from '@/config';
|
||||
import * as os from '@/os';
|
||||
import { mainRouter, routes } from '@/router';
|
||||
import { i18n } from '@/i18n';
|
||||
import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata';
|
||||
import { Router } from '@/nirax';
|
||||
|
||||
const props = defineProps<{
|
||||
initialPath: string;
|
||||
}>();
|
||||
|
||||
defineEmits<{
|
||||
(ev: 'closed'): void;
|
||||
(ev: 'click'): void;
|
||||
}>();
|
||||
|
||||
const router = new Router(routes, props.initialPath);
|
||||
|
||||
router.addListener('push', ctx => {
|
||||
|
||||
});
|
||||
|
||||
let pageMetadata = $ref<null | ComputedRef<PageMetadata>>();
|
||||
let rootEl = $ref();
|
||||
let modal = $shallowRef<InstanceType<typeof MkModal>>();
|
||||
let path = $ref(props.initialPath);
|
||||
let width = $ref(860);
|
||||
let height = $ref(660);
|
||||
const history = [];
|
||||
|
||||
provide('router', router);
|
||||
provideMetadataReceiver((info) => {
|
||||
pageMetadata = info;
|
||||
});
|
||||
provide('shouldOmitHeaderTitle', true);
|
||||
provide('shouldHeaderThin', true);
|
||||
|
||||
const pageUrl = $computed(() => url + path);
|
||||
const contextmenu = $computed(() => {
|
||||
return [{
|
||||
type: 'label',
|
||||
text: path,
|
||||
}, {
|
||||
icon: 'ti ti-player-eject',
|
||||
text: i18n.ts.showInPage,
|
||||
action: expand,
|
||||
}, {
|
||||
icon: 'ti ti-window-maximize',
|
||||
text: i18n.ts.popout,
|
||||
action: popout,
|
||||
}, null, {
|
||||
icon: 'ti ti-external-link',
|
||||
text: i18n.ts.openInNewTab,
|
||||
action: () => {
|
||||
window.open(pageUrl, '_blank');
|
||||
modal.close();
|
||||
},
|
||||
}, {
|
||||
icon: 'ti ti-link',
|
||||
text: i18n.ts.copyLink,
|
||||
action: () => {
|
||||
copyToClipboard(pageUrl);
|
||||
},
|
||||
}];
|
||||
});
|
||||
|
||||
function navigate(path, record = true) {
|
||||
if (record) history.push(router.getCurrentPath());
|
||||
router.push(path);
|
||||
}
|
||||
|
||||
function back() {
|
||||
navigate(history.pop(), false);
|
||||
}
|
||||
|
||||
function expand() {
|
||||
mainRouter.push(path);
|
||||
modal.close();
|
||||
}
|
||||
|
||||
function popout() {
|
||||
_popout(path, rootEl);
|
||||
modal.close();
|
||||
}
|
||||
|
||||
function onContextmenu(ev: MouseEvent) {
|
||||
os.contextMenu(contextmenu, ev);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.hrmcaedk {
|
||||
margin: auto;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
contain: content;
|
||||
border-radius: var(--radius);
|
||||
|
||||
--root-margin: 24px;
|
||||
|
||||
@media (max-width: 500px) {
|
||||
--root-margin: 16px;
|
||||
}
|
||||
|
||||
> .header {
|
||||
$height: 52px;
|
||||
$height-narrow: 42px;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
height: $height;
|
||||
line-height: $height;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
background: var(--windowHeader);
|
||||
-webkit-backdrop-filter: var(--blur, blur(15px));
|
||||
backdrop-filter: var(--blur, blur(15px));
|
||||
|
||||
> button {
|
||||
height: $height;
|
||||
width: $height;
|
||||
|
||||
&:hover {
|
||||
color: var(--fgHighlighted);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 500px) {
|
||||
height: $height-narrow;
|
||||
line-height: $height-narrow;
|
||||
padding-left: 16px;
|
||||
|
||||
> button {
|
||||
height: $height-narrow;
|
||||
width: $height-narrow;
|
||||
}
|
||||
}
|
||||
|
||||
> .title {
|
||||
flex: 1;
|
||||
|
||||
> .icon {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .body {
|
||||
overflow: auto;
|
||||
background: var(--bg);
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<MkModal ref="modal" :prefer-type="'dialog'" @click="onBgClick" @closed="$emit('closed')">
|
||||
<MkModal ref="modal" :preferType="'dialog'" @click="onBgClick" @closed="$emit('closed')">
|
||||
<div ref="rootEl" :class="$style.root" :style="{ width: `${width}px`, height: `min(${height}px, 100%)` }" @keydown="onKeydown">
|
||||
<div ref="headerEl" :class="$style.header">
|
||||
<button v-if="withOkButton" :class="$style.headerButton" class="_button" @click="$emit('close')"><i class="ti ti-x"></i></button>
|
||||
|
@@ -44,8 +44,8 @@
|
||||
<div v-if="appearNote.channel" :class="$style.colorBar" :style="{ background: appearNote.channel.color }"></div>
|
||||
<MkAvatar :class="$style.avatar" :user="appearNote.user" link preview/>
|
||||
<div :class="$style.main">
|
||||
<MkNoteHeader :class="$style.header" :note="appearNote" :mini="true"/>
|
||||
<MkInstanceTicker v-if="showTicker" :class="$style.ticker" :instance="appearNote.user.instance"/>
|
||||
<MkNoteHeader :note="appearNote" :mini="true"/>
|
||||
<MkInstanceTicker v-if="showTicker" :instance="appearNote.user.instance"/>
|
||||
<div style="container-type: inline-size;">
|
||||
<p v-if="appearNote.cw != null" :class="$style.cw">
|
||||
<Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :i="$i"/>
|
||||
@@ -55,17 +55,17 @@
|
||||
<div :class="$style.text">
|
||||
<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
|
||||
<MkA v-if="appearNote.replyId" :class="$style.replyIcon" :to="`/notes/${appearNote.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
|
||||
<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :emoji-urls="appearNote.emojis"/>
|
||||
<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :emojiUrls="appearNote.emojis"/>
|
||||
<div v-if="translating || translation" :class="$style.translation">
|
||||
<MkLoading v-if="translating" mini/>
|
||||
<div v-else :class="$style.translated">
|
||||
<div v-else>
|
||||
<b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}: </b>
|
||||
<Mfm :text="translation.text" :author="appearNote.user" :i="$i" :emoji-urls="appearNote.emojis"/>
|
||||
<Mfm :text="translation.text" :author="appearNote.user" :i="$i" :emojiUrls="appearNote.emojis"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="appearNote.files.length > 0" :class="$style.files">
|
||||
<MkMediaList :media-list="appearNote.files"/>
|
||||
<div v-if="appearNote.files.length > 0">
|
||||
<MkMediaList :mediaList="appearNote.files"/>
|
||||
</div>
|
||||
<MkPoll v-if="appearNote.poll" :note="appearNote" :class="$style.poll"/>
|
||||
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :class="$style.urlPreview"/>
|
||||
@@ -79,7 +79,7 @@
|
||||
</div>
|
||||
<MkA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ti ti-device-tv"></i> {{ appearNote.channel.name }}</MkA>
|
||||
</div>
|
||||
<MkReactionsViewer :note="appearNote" :max-number="16">
|
||||
<MkReactionsViewer :note="appearNote" :maxNumber="16">
|
||||
<template #more>
|
||||
<button class="_button" :class="$style.reactionDetailsButton" @click="showReactions">
|
||||
{{ i18n.ts.more }}
|
||||
@@ -205,8 +205,11 @@ const isMyRenote = $i && ($i.id === note.userId);
|
||||
const showContent = ref(false);
|
||||
const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)) : null;
|
||||
const isLong = (appearNote.cw == null && appearNote.text != null && (
|
||||
(appearNote.text.includes('$[x2')) ||
|
||||
(appearNote.text.includes('$[x3')) ||
|
||||
(appearNote.text.includes('$[x4')) ||
|
||||
(appearNote.text.includes('$[scale')) ||
|
||||
(appearNote.text.includes('$[position')) ||
|
||||
(appearNote.text.split('\n').length > 9) ||
|
||||
(appearNote.text.length > 500) ||
|
||||
(appearNote.files.length >= 5) ||
|
||||
@@ -274,7 +277,7 @@ function renote(viaKeyboard = false) {
|
||||
const y = rect.top + (el.offsetHeight / 2);
|
||||
os.popup(MkRippleEffect, { x, y }, {}, 'end');
|
||||
}
|
||||
|
||||
|
||||
os.api('notes/create', {
|
||||
renoteId: appearNote.id,
|
||||
channelId: appearNote.channelId,
|
||||
@@ -305,7 +308,7 @@ function renote(viaKeyboard = false) {
|
||||
const y = rect.top + (el.offsetHeight / 2);
|
||||
os.popup(MkRippleEffect, { x, y }, {}, 'end');
|
||||
}
|
||||
|
||||
|
||||
os.api('notes/create', {
|
||||
renoteId: appearNote.id,
|
||||
}).then(() => {
|
||||
@@ -379,6 +382,8 @@ function undoReact(note): void {
|
||||
function onContextmenu(ev: MouseEvent): void {
|
||||
const isLink = (el: HTMLElement) => {
|
||||
if (el.tagName === 'A') return true;
|
||||
// 再生速度の選択などのために、Audio要素のコンテキストメニューはブラウザデフォルトとする。
|
||||
if (el.tagName === 'AUDIO') return true;
|
||||
if (el.parentElement) {
|
||||
return isLink(el.parentElement);
|
||||
}
|
||||
|
@@ -4,25 +4,25 @@
|
||||
v-show="!isDeleted"
|
||||
ref="el"
|
||||
v-hotkey="keymap"
|
||||
class="lxwezrsl"
|
||||
:tabindex="!isDeleted ? '-1' : null"
|
||||
:class="{ renote: isRenote }"
|
||||
:class="$style.root"
|
||||
>
|
||||
<MkNoteSub v-for="note in conversation" :key="note.id" class="reply-to-more" :note="note"/>
|
||||
<MkNoteSub v-if="appearNote.reply" :note="appearNote.reply" class="reply-to"/>
|
||||
<div v-if="isRenote" class="renote">
|
||||
<MkAvatar class="avatar" :user="note.user" link preview/>
|
||||
<i class="ti ti-repeat"></i>
|
||||
<I18n :src="i18n.ts.renotedBy" tag="span">
|
||||
<template #user>
|
||||
<MkA v-user-preview="note.userId" class="name" :to="userPage(note.user)">
|
||||
<MkUserName :user="note.user"/>
|
||||
</MkA>
|
||||
</template>
|
||||
</I18n>
|
||||
<div class="info">
|
||||
<button ref="renoteTime" class="_button time" @click="showRenoteMenu()">
|
||||
<i v-if="isMyRenote" class="ti ti-dots dropdownIcon"></i>
|
||||
<MkNoteSub v-for="note in conversation" :key="note.id" :class="$style.replyToMore" :note="note"/>
|
||||
<MkNoteSub v-if="appearNote.reply" :note="appearNote.reply" :class="$style.replyTo"/>
|
||||
<div v-if="isRenote" :class="$style.renote">
|
||||
<MkAvatar :class="$style.renoteAvatar" :user="note.user" link preview/>
|
||||
<i class="ti ti-repeat" style="margin-right: 4px;"></i>
|
||||
<span :class="$style.renoteText">
|
||||
<I18n :src="i18n.ts.renotedBy" tag="span">
|
||||
<template #user>
|
||||
<MkA v-user-preview="note.userId" :class="$style.renoteName" :to="userPage(note.user)">
|
||||
<MkUserName :user="note.user"/>
|
||||
</MkA>
|
||||
</template>
|
||||
</I18n>
|
||||
</span>
|
||||
<div :class="$style.renoteInfo">
|
||||
<button ref="renoteTime" class="_button" :class="$style.renoteTime" @click="showRenoteMenu()">
|
||||
<i v-if="isMyRenote" class="ti ti-dots" style="margin-right: 4px;"></i>
|
||||
<MkTime :time="note.createdAt"/>
|
||||
</button>
|
||||
<span v-if="note.visibility !== 'public'" style="margin-left: 0.5em;" :title="i18n.ts._visibility[note.visibility]">
|
||||
@@ -33,16 +33,16 @@
|
||||
<span v-if="note.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-rocket-off"></i></span>
|
||||
</div>
|
||||
</div>
|
||||
<article class="article" @contextmenu.stop="onContextmenu">
|
||||
<header class="header">
|
||||
<MkAvatar class="avatar" :user="appearNote.user" indicator link preview/>
|
||||
<div class="body">
|
||||
<div class="top">
|
||||
<MkA v-user-preview="appearNote.user.id" class="name" :to="userPage(appearNote.user)">
|
||||
<article :class="$style.note" @contextmenu.stop="onContextmenu">
|
||||
<header :class="$style.noteHeader">
|
||||
<MkAvatar :class="$style.noteHeaderAvatar" :user="appearNote.user" indicator link preview/>
|
||||
<div :class="$style.noteHeaderBody">
|
||||
<div>
|
||||
<MkA v-user-preview="appearNote.user.id" :class="$style.noteHeaderName" :to="userPage(appearNote.user)">
|
||||
<MkUserName :nowrap="false" :user="appearNote.user"/>
|
||||
</MkA>
|
||||
<span v-if="appearNote.user.isBot" class="is-bot">bot</span>
|
||||
<div class="info">
|
||||
<span v-if="appearNote.user.isBot" :class="$style.isBot">bot</span>
|
||||
<div :class="$style.noteHeaderInfo">
|
||||
<span v-if="appearNote.visibility !== 'public'" style="margin-left: 0.5em;" :title="i18n.ts._visibility[appearNote.visibility]">
|
||||
<i v-if="appearNote.visibility === 'home'" class="ti ti-home"></i>
|
||||
<i v-else-if="appearNote.visibility === 'followers'" class="ti ti-lock"></i>
|
||||
@@ -51,84 +51,81 @@
|
||||
<span v-if="appearNote.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-rocket-off"></i></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="username"><MkAcct :user="appearNote.user"/></div>
|
||||
<MkInstanceTicker v-if="showTicker" class="ticker" :instance="appearNote.user.instance"/>
|
||||
<div :class="$style.noteHeaderUsername"><MkAcct :user="appearNote.user"/></div>
|
||||
<MkInstanceTicker v-if="showTicker" :instance="appearNote.user.instance"/>
|
||||
</div>
|
||||
</header>
|
||||
<div class="main">
|
||||
<div class="body">
|
||||
<p v-if="appearNote.cw != null" class="cw">
|
||||
<Mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$i"/>
|
||||
<MkCwButton v-model="showContent" :note="appearNote"/>
|
||||
</p>
|
||||
<div v-show="appearNote.cw == null || showContent" class="content">
|
||||
<div class="text">
|
||||
<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
|
||||
<MkA v-if="appearNote.replyId" class="reply" :to="`/notes/${appearNote.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
|
||||
<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :emoji-urls="appearNote.emojis"/>
|
||||
<a v-if="appearNote.renote != null" class="rp">RN:</a>
|
||||
<div v-if="translating || translation" class="translation">
|
||||
<MkLoading v-if="translating" mini/>
|
||||
<div v-else class="translated">
|
||||
<b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}: </b>
|
||||
<Mfm :text="translation.text" :author="appearNote.user" :i="$i" :emoji-urls="appearNote.emojis"/>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.noteContent">
|
||||
<p v-if="appearNote.cw != null" :class="$style.cw">
|
||||
<Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :i="$i"/>
|
||||
<MkCwButton v-model="showContent" :note="appearNote"/>
|
||||
</p>
|
||||
<div v-show="appearNote.cw == null || showContent">
|
||||
<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
|
||||
<MkA v-if="appearNote.replyId" :class="$style.noteReplyTarget" :to="`/notes/${appearNote.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
|
||||
<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :emojiUrls="appearNote.emojis"/>
|
||||
<a v-if="appearNote.renote != null" :class="$style.rn">RN:</a>
|
||||
<div v-if="translating || translation" :class="$style.translation">
|
||||
<MkLoading v-if="translating" mini/>
|
||||
<div v-else>
|
||||
<b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}: </b>
|
||||
<Mfm :text="translation.text" :author="appearNote.user" :i="$i" :emojiUrls="appearNote.emojis"/>
|
||||
</div>
|
||||
<div v-if="appearNote.files.length > 0" class="files">
|
||||
<MkMediaList :media-list="appearNote.files"/>
|
||||
</div>
|
||||
<MkPoll v-if="appearNote.poll" ref="pollViewer" :note="appearNote" class="poll"/>
|
||||
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="true" class="url-preview"/>
|
||||
<div v-if="appearNote.renote" class="renote"><MkNoteSimple :note="appearNote.renote" class="note"/></div>
|
||||
</div>
|
||||
<MkA v-if="appearNote.channel && !inChannel" class="channel" :to="`/channels/${appearNote.channel.id}`"><i class="ti ti-device-tv"></i> {{ appearNote.channel.name }}</MkA>
|
||||
<div v-if="appearNote.files.length > 0">
|
||||
<MkMediaList :mediaList="appearNote.files"/>
|
||||
</div>
|
||||
<MkPoll v-if="appearNote.poll" ref="pollViewer" :note="appearNote" :class="$style.poll"/>
|
||||
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="true" style="margin-top: 6px;"/>
|
||||
<div v-if="appearNote.renote" :class="$style.quote"><MkNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></div>
|
||||
</div>
|
||||
<footer class="footer">
|
||||
<div class="info">
|
||||
<MkA class="created-at" :to="notePage(appearNote)">
|
||||
<MkTime :time="appearNote.createdAt" mode="detail"/>
|
||||
</MkA>
|
||||
</div>
|
||||
<MkReactionsViewer ref="reactionsViewer" :note="appearNote"/>
|
||||
<button class="button _button" @click="reply()">
|
||||
<i class="ti ti-arrow-back-up"></i>
|
||||
<p v-if="appearNote.repliesCount > 0" class="count">{{ appearNote.repliesCount }}</p>
|
||||
</button>
|
||||
<button
|
||||
v-if="canRenote"
|
||||
ref="renoteButton"
|
||||
class="button _button"
|
||||
@mousedown="renote()"
|
||||
>
|
||||
<i class="ti ti-repeat"></i>
|
||||
<p v-if="appearNote.renoteCount > 0" class="count">{{ appearNote.renoteCount }}</p>
|
||||
</button>
|
||||
<button v-else class="button _button" disabled>
|
||||
<i class="ti ti-ban"></i>
|
||||
</button>
|
||||
<button v-if="appearNote.myReaction == null" ref="reactButton" class="button _button" @mousedown="react()">
|
||||
<i v-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
|
||||
<i v-else class="ti ti-plus"></i>
|
||||
</button>
|
||||
<button v-if="appearNote.myReaction != null" ref="reactButton" class="button _button reacted" @click="undoReact(appearNote)">
|
||||
<i class="ti ti-minus"></i>
|
||||
</button>
|
||||
<button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" class="button _button" @mousedown="clip()">
|
||||
<i class="ti ti-paperclip"></i>
|
||||
</button>
|
||||
<button ref="menuButton" class="button _button" @mousedown="menu()">
|
||||
<i class="ti ti-dots"></i>
|
||||
</button>
|
||||
</footer>
|
||||
<MkA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ti ti-device-tv"></i> {{ appearNote.channel.name }}</MkA>
|
||||
</div>
|
||||
<footer>
|
||||
<div :class="$style.noteFooterInfo">
|
||||
<MkA :to="notePage(appearNote)">
|
||||
<MkTime :time="appearNote.createdAt" mode="detail"/>
|
||||
</MkA>
|
||||
</div>
|
||||
<MkReactionsViewer ref="reactionsViewer" :note="appearNote"/>
|
||||
<button class="_button" :class="$style.noteFooterButton" @click="reply()">
|
||||
<i class="ti ti-arrow-back-up"></i>
|
||||
<p v-if="appearNote.repliesCount > 0" :class="$style.noteFooterButtonCount">{{ appearNote.repliesCount }}</p>
|
||||
</button>
|
||||
<button
|
||||
v-if="canRenote"
|
||||
ref="renoteButton"
|
||||
class="_button"
|
||||
:class="$style.noteFooterButton"
|
||||
@mousedown="renote()"
|
||||
>
|
||||
<i class="ti ti-repeat"></i>
|
||||
<p v-if="appearNote.renoteCount > 0" :class="$style.noteFooterButtonCount">{{ appearNote.renoteCount }}</p>
|
||||
</button>
|
||||
<button v-else class="_button" :class="$style.noteFooterButton" disabled>
|
||||
<i class="ti ti-ban"></i>
|
||||
</button>
|
||||
<button v-if="appearNote.myReaction == null" ref="reactButton" :class="$style.noteFooterButton" class="_button" @mousedown="react()">
|
||||
<i v-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
|
||||
<i v-else class="ti ti-plus"></i>
|
||||
</button>
|
||||
<button v-if="appearNote.myReaction != null" ref="reactButton" class="_button" :class="[$style.noteFooterButton, $style.reacted]" @click="undoReact(appearNote)">
|
||||
<i class="ti ti-minus"></i>
|
||||
</button>
|
||||
<button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" class="_button" :class="$style.noteFooterButton" @mousedown="clip()">
|
||||
<i class="ti ti-paperclip"></i>
|
||||
</button>
|
||||
<button ref="menuButton" class="_button" :class="$style.noteFooterButton" @mousedown="menu()">
|
||||
<i class="ti ti-dots"></i>
|
||||
</button>
|
||||
</footer>
|
||||
</article>
|
||||
<MkNoteSub v-for="note in replies" :key="note.id" :note="note" class="reply" :detail="true"/>
|
||||
<MkNoteSub v-for="note in replies" :key="note.id" :note="note" :class="$style.reply" :detail="true"/>
|
||||
</div>
|
||||
<div v-else class="_panel muted" @click="muted = false">
|
||||
<div v-else class="_panel" :class="$style.muted" @click="muted = false">
|
||||
<I18n :src="i18n.ts.userSaysSomething" tag="small">
|
||||
<template #name>
|
||||
<MkA v-user-preview="appearNote.userId" class="name" :to="userPage(appearNote.user)">
|
||||
<MkA v-user-preview="appearNote.userId" :to="userPage(appearNote.user)">
|
||||
<MkUserName :user="appearNote.user"/>
|
||||
</MkA>
|
||||
</template>
|
||||
@@ -438,318 +435,249 @@ if (appearNote.replyId) {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.lxwezrsl {
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
position: relative;
|
||||
transition: box-shadow 0.1s ease;
|
||||
overflow: clip;
|
||||
contain: content;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
.replyTo {
|
||||
opacity: 0.7;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
&:after {
|
||||
content: "";
|
||||
pointer-events: none;
|
||||
display: block;
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
margin: auto;
|
||||
width: calc(100% - 8px);
|
||||
height: calc(100% - 8px);
|
||||
border: dashed 1px var(--focus);
|
||||
border-radius: var(--radius);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
.replyToMore {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
&:hover > .article > .main > .footer > .button {
|
||||
.renote {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16px 32px 8px 32px;
|
||||
line-height: 28px;
|
||||
white-space: pre;
|
||||
color: var(--renote);
|
||||
}
|
||||
|
||||
.renoteAvatar {
|
||||
flex-shrink: 0;
|
||||
display: inline-block;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
margin: 0 8px 0 0;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.renoteText {
|
||||
overflow: hidden;
|
||||
flex-shrink: 1;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.renoteName {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.renoteInfo {
|
||||
margin-left: auto;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.renoteTime {
|
||||
flex-shrink: 0;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.renote + .note {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.note {
|
||||
padding: 32px;
|
||||
font-size: 1.2em;
|
||||
|
||||
&:hover > .main > .footer > .button {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
> .reply-to {
|
||||
opacity: 0.7;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
> .reply-to-more {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
> .renote {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16px 32px 8px 32px;
|
||||
line-height: 28px;
|
||||
white-space: pre;
|
||||
color: var(--renote);
|
||||
|
||||
> .avatar {
|
||||
flex-shrink: 0;
|
||||
display: inline-block;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
margin: 0 8px 0 0;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
> i {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
> span {
|
||||
overflow: hidden;
|
||||
flex-shrink: 1;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
> .name {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
> .info {
|
||||
margin-left: auto;
|
||||
font-size: 0.9em;
|
||||
|
||||
> .time {
|
||||
flex-shrink: 0;
|
||||
color: inherit;
|
||||
|
||||
> .dropdownIcon {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .renote + .article {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
> .article {
|
||||
padding: 32px;
|
||||
font-size: 1.2em;
|
||||
|
||||
> .header {
|
||||
display: flex;
|
||||
position: relative;
|
||||
margin-bottom: 16px;
|
||||
align-items: center;
|
||||
|
||||
> .avatar {
|
||||
display: block;
|
||||
flex-shrink: 0;
|
||||
width: 58px;
|
||||
height: 58px;
|
||||
}
|
||||
|
||||
> .body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding-left: 16px;
|
||||
font-size: 0.95em;
|
||||
|
||||
> .top {
|
||||
> .name {
|
||||
font-weight: bold;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
> .is-bot {
|
||||
display: inline-block;
|
||||
margin: 0 0.5em;
|
||||
padding: 4px 6px;
|
||||
font-size: 80%;
|
||||
line-height: 1;
|
||||
border: solid 0.5px var(--divider);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
> .info {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
|
||||
> .username {
|
||||
margin-bottom: 2px;
|
||||
line-height: 1.3;
|
||||
word-wrap: anywhere;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .main {
|
||||
> .body {
|
||||
container-type: inline-size;
|
||||
|
||||
> .cw {
|
||||
cursor: default;
|
||||
display: block;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow-wrap: break-word;
|
||||
|
||||
> .text {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
> .content {
|
||||
> .text {
|
||||
overflow-wrap: break-word;
|
||||
|
||||
> .reply {
|
||||
color: var(--accent);
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
> .rp {
|
||||
margin-left: 4px;
|
||||
font-style: oblique;
|
||||
color: var(--renote);
|
||||
}
|
||||
|
||||
> .translation {
|
||||
border: solid 0.5px var(--divider);
|
||||
border-radius: var(--radius);
|
||||
padding: 12px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
> .url-preview {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
> .poll {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
> .renote {
|
||||
padding: 8px 0;
|
||||
|
||||
> .note {
|
||||
padding: 16px;
|
||||
border: dashed 1px var(--renote);
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .channel {
|
||||
opacity: 0.7;
|
||||
font-size: 80%;
|
||||
}
|
||||
}
|
||||
|
||||
> .footer {
|
||||
> .info {
|
||||
margin: 16px 0;
|
||||
opacity: 0.7;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
> .button {
|
||||
margin: 0;
|
||||
padding: 8px;
|
||||
opacity: 0.7;
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-right: 28px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--fgHighlighted);
|
||||
}
|
||||
|
||||
> .count {
|
||||
display: inline;
|
||||
margin: 0 0 0 8px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
&.reacted {
|
||||
color: var(--accent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .reply {
|
||||
border-top: solid 0.5px var(--divider);
|
||||
}
|
||||
}
|
||||
|
||||
.noteHeader {
|
||||
display: flex;
|
||||
position: relative;
|
||||
margin-bottom: 16px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.noteHeaderAvatar {
|
||||
display: block;
|
||||
flex-shrink: 0;
|
||||
width: 58px;
|
||||
height: 58px;
|
||||
}
|
||||
|
||||
.noteHeaderBody {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding-left: 16px;
|
||||
font-size: 0.95em;
|
||||
}
|
||||
|
||||
.noteHeaderName {
|
||||
font-weight: bold;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.isBot {
|
||||
display: inline-block;
|
||||
margin: 0 0.5em;
|
||||
padding: 4px 6px;
|
||||
font-size: 80%;
|
||||
line-height: 1;
|
||||
border: solid 0.5px var(--divider);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.noteHeaderInfo {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.noteHeaderUsername {
|
||||
margin-bottom: 2px;
|
||||
line-height: 1.3;
|
||||
word-wrap: anywhere;
|
||||
}
|
||||
|
||||
.noteContent {
|
||||
container-type: inline-size;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.cw {
|
||||
cursor: default;
|
||||
display: block;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.noteReplyTarget {
|
||||
color: var(--accent);
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
.rn {
|
||||
margin-left: 4px;
|
||||
font-style: oblique;
|
||||
color: var(--renote);
|
||||
}
|
||||
|
||||
.translation {
|
||||
border: solid 0.5px var(--divider);
|
||||
border-radius: var(--radius);
|
||||
padding: 12px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.poll {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
.quote {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.quoteNote {
|
||||
padding: 16px;
|
||||
border: dashed 1px var(--renote);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.channel {
|
||||
opacity: 0.7;
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
.noteFooterInfo {
|
||||
margin: 16px 0;
|
||||
opacity: 0.7;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.noteFooterButton {
|
||||
margin: 0;
|
||||
padding: 8px;
|
||||
opacity: 0.7;
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-right: 28px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--fgHighlighted);
|
||||
}
|
||||
}
|
||||
|
||||
.noteFooterButtonCount {
|
||||
display: inline;
|
||||
margin: 0 0 0 8px;
|
||||
opacity: 0.7;
|
||||
|
||||
&.reacted {
|
||||
color: var(--accent);
|
||||
}
|
||||
}
|
||||
|
||||
.reply {
|
||||
border-top: solid 0.5px var(--divider);
|
||||
}
|
||||
|
||||
@container (max-width: 500px) {
|
||||
.lxwezrsl {
|
||||
.root {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
}
|
||||
|
||||
@container (max-width: 450px) {
|
||||
.lxwezrsl {
|
||||
> .renote {
|
||||
padding: 8px 16px 0 16px;
|
||||
}
|
||||
.renote {
|
||||
padding: 8px 16px 0 16px;
|
||||
}
|
||||
|
||||
> .article {
|
||||
padding: 16px;
|
||||
.note {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
> .header {
|
||||
> .avatar {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.noteHeaderAvatar {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
@container (max-width: 350px) {
|
||||
.lxwezrsl {
|
||||
> .article {
|
||||
> .main {
|
||||
> .footer {
|
||||
> .button {
|
||||
&:not(:last-child) {
|
||||
margin-right: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.noteFooterButton {
|
||||
&:not(:last-child) {
|
||||
margin-right: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@container (max-width: 300px) {
|
||||
.lxwezrsl {
|
||||
.root {
|
||||
font-size: 0.825em;
|
||||
}
|
||||
|
||||
> .article {
|
||||
> .header {
|
||||
> .avatar {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
}
|
||||
.noteHeaderAvatar {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
> .main {
|
||||
> .footer {
|
||||
> .button {
|
||||
&:not(:last-child) {
|
||||
margin-right: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.noteFooterButton {
|
||||
&:not(:last-child) {
|
||||
margin-right: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -6,7 +6,7 @@
|
||||
<MkUserName :user="$i" :nowrap="true"/>
|
||||
</div>
|
||||
<div>
|
||||
<div :class="$style.content">
|
||||
<div>
|
||||
<Mfm :text="text.trim()" :author="$i" :i="$i"/>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -5,7 +5,7 @@
|
||||
<MkNoteHeader :class="$style.header" :note="note" :mini="true"/>
|
||||
<div>
|
||||
<p v-if="note.cw != null" :class="$style.cw">
|
||||
<Mfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :author="note.user" :i="$i" :emoji-urls="note.emojis"/>
|
||||
<Mfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :author="note.user" :i="$i" :emojiUrls="note.emojis"/>
|
||||
<MkCwButton v-model="showContent" :note="note"/>
|
||||
</p>
|
||||
<div v-show="note.cw == null || showContent">
|
||||
|
@@ -2,7 +2,7 @@
|
||||
<MkPagination ref="pagingComponent" :pagination="pagination">
|
||||
<template #empty>
|
||||
<div class="_fullinfo">
|
||||
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
|
||||
<img :src="infoImageUrl" class="_ghost"/>
|
||||
<div>{{ i18n.ts.noNotes }}</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -15,7 +15,7 @@
|
||||
:items="notes"
|
||||
:direction="pagination.reversed ? 'up' : 'down'"
|
||||
:reversed="pagination.reversed"
|
||||
:no-gap="noGap"
|
||||
:noGap="noGap"
|
||||
:ad="true"
|
||||
:class="$style.notes"
|
||||
>
|
||||
@@ -32,6 +32,7 @@ import MkNote from '@/components/MkNote.vue';
|
||||
import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
|
||||
import MkPagination, { Paging } from '@/components/MkPagination.vue';
|
||||
import { i18n } from '@/i18n';
|
||||
import { infoImageUrl } from '@/instance';
|
||||
|
||||
const props = defineProps<{
|
||||
pagination: Paging;
|
||||
|
@@ -5,7 +5,19 @@
|
||||
<MkAvatar v-else-if="notification.type === 'achievementEarned'" :class="$style.icon" :user="$i" link preview/>
|
||||
<MkAvatar v-else-if="notification.user" :class="$style.icon" :user="notification.user" link preview/>
|
||||
<img v-else-if="notification.icon" :class="$style.icon" :src="notification.icon" alt=""/>
|
||||
<div :class="[$style.subIcon, $style['t_' + notification.type]]">
|
||||
<div
|
||||
:class="[$style.subIcon, {
|
||||
[$style.t_follow]: notification.type === 'follow',
|
||||
[$style.t_followRequestAccepted]: notification.type === 'followRequestAccepted',
|
||||
[$style.t_receiveFollowRequest]: notification.type === 'receiveFollowRequest',
|
||||
[$style.t_renote]: notification.type === 'renote',
|
||||
[$style.t_reply]: notification.type === 'reply',
|
||||
[$style.t_mention]: notification.type === 'mention',
|
||||
[$style.t_quote]: notification.type === 'quote',
|
||||
[$style.t_pollEnded]: notification.type === 'pollEnded',
|
||||
[$style.t_achievementEarned]: notification.type === 'achievementEarned',
|
||||
}]"
|
||||
>
|
||||
<i v-if="notification.type === 'follow'" class="ti ti-plus"></i>
|
||||
<i v-else-if="notification.type === 'receiveFollowRequest'" class="ti ti-clock"></i>
|
||||
<i v-else-if="notification.type === 'followRequestAccepted'" class="ti ti-check"></i>
|
||||
@@ -20,8 +32,8 @@
|
||||
v-else-if="notification.type === 'reaction'"
|
||||
ref="reactionRef"
|
||||
:reaction="notification.reaction ? notification.reaction.replace(/^:(\w+):$/, ':$1@.:') : notification.reaction"
|
||||
:custom-emojis="notification.note.emojis"
|
||||
:no-style="true"
|
||||
:customEmojis="notification.note.emojis"
|
||||
:noStyle="true"
|
||||
style="width: 100%; height: 100%;"
|
||||
/>
|
||||
</div>
|
||||
@@ -34,7 +46,7 @@
|
||||
<span v-else>{{ notification.header }}</span>
|
||||
<MkTime v-if="withTime" :time="notification.createdAt" :class="$style.headerTime"/>
|
||||
</header>
|
||||
<div :class="$style.content">
|
||||
<div>
|
||||
<MkA v-if="notification.type === 'reaction'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
||||
<i class="ti ti-quote" :class="$style.quote"></i>
|
||||
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :author="notification.note.user"/>
|
||||
@@ -243,9 +255,6 @@ useTooltip(reactionRef, (showing) => {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.content {
|
||||
}
|
||||
|
||||
.text {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
|
@@ -3,15 +3,15 @@
|
||||
ref="dialog"
|
||||
:width="400"
|
||||
:height="450"
|
||||
:with-ok-button="true"
|
||||
:ok-button-disabled="false"
|
||||
:withOkButton="true"
|
||||
:okButtonDisabled="false"
|
||||
@ok="ok()"
|
||||
@close="dialog?.close()"
|
||||
@closed="emit('closed')"
|
||||
>
|
||||
<template #header>{{ i18n.ts.notificationSetting }}</template>
|
||||
|
||||
<MkSpacer :margin-min="20" :margin-max="28">
|
||||
<MkSpacer :marginMin="20" :marginMax="28">
|
||||
<div class="_gaps_m">
|
||||
<template v-if="showGlobalToggle">
|
||||
<MkSwitch v-model="useGlobalSetting">
|
||||
|
@@ -2,15 +2,15 @@
|
||||
<MkPagination ref="pagingComponent" :pagination="pagination">
|
||||
<template #empty>
|
||||
<div class="_fullinfo">
|
||||
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
|
||||
<img :src="infoImageUrl" class="_ghost"/>
|
||||
<div>{{ i18n.ts.noNotifications }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #default="{ items: notifications }">
|
||||
<MkDateSeparatedList v-slot="{ item: notification }" :class="$style.list" :items="notifications" :no-gap="true">
|
||||
<MkDateSeparatedList v-slot="{ item: notification }" :class="$style.list" :items="notifications" :noGap="true">
|
||||
<MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note"/>
|
||||
<XNotification v-else :key="notification.id" :notification="notification" :with-time="true" :full="true" class="_panel notification"/>
|
||||
<XNotification v-else :key="notification.id" :notification="notification" :withTime="true" :full="true" class="_panel notification"/>
|
||||
</MkDateSeparatedList>
|
||||
</template>
|
||||
</MkPagination>
|
||||
@@ -22,10 +22,11 @@ import MkPagination, { Paging } from '@/components/MkPagination.vue';
|
||||
import XNotification from '@/components/MkNotification.vue';
|
||||
import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
|
||||
import MkNote from '@/components/MkNote.vue';
|
||||
import { stream } from '@/stream';
|
||||
import { useStream } from '@/stream';
|
||||
import { $i } from '@/account';
|
||||
import { i18n } from '@/i18n';
|
||||
import { notificationTypes } from '@/const';
|
||||
import { infoImageUrl } from '@/instance';
|
||||
|
||||
const props = defineProps<{
|
||||
includeTypes?: typeof notificationTypes[number][];
|
||||
@@ -45,7 +46,7 @@ const pagination: Paging = {
|
||||
const onNotification = (notification) => {
|
||||
const isMuted = props.includeTypes ? !props.includeTypes.includes(notification.type) : $i.mutingNotificationTypes.includes(notification.type);
|
||||
if (isMuted || document.visibilityState === 'visible') {
|
||||
stream.send('readNotification');
|
||||
useStream().send('readNotification');
|
||||
}
|
||||
|
||||
if (!isMuted) {
|
||||
@@ -56,7 +57,7 @@ const onNotification = (notification) => {
|
||||
let connection;
|
||||
|
||||
onMounted(() => {
|
||||
connection = stream.useChannel('main');
|
||||
connection = useStream().useChannel('main');
|
||||
connection.on('notification', onNotification);
|
||||
});
|
||||
|
||||
|
@@ -28,54 +28,38 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, reactive } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { reactive } from 'vue';
|
||||
import number from '@/filters/number';
|
||||
import XValue from '@/components/MkObjectView.value.vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'XValue',
|
||||
const props = defineProps<{
|
||||
value: any;
|
||||
}>();
|
||||
|
||||
props: {
|
||||
value: {
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
const collapsed = reactive({});
|
||||
|
||||
setup(props) {
|
||||
const collapsed = reactive({});
|
||||
if (isObject(props.value)) {
|
||||
for (const key in props.value) {
|
||||
collapsed[key] = collapsable(props.value[key]);
|
||||
}
|
||||
}
|
||||
|
||||
if (isObject(props.value)) {
|
||||
for (const key in props.value) {
|
||||
collapsed[key] = collapsable(props.value[key]);
|
||||
}
|
||||
}
|
||||
function isObject(v): boolean {
|
||||
return typeof v === 'object' && !Array.isArray(v) && v !== null;
|
||||
}
|
||||
|
||||
function isObject(v): boolean {
|
||||
return typeof v === 'object' && !Array.isArray(v) && v !== null;
|
||||
}
|
||||
function isArray(v): boolean {
|
||||
return Array.isArray(v);
|
||||
}
|
||||
|
||||
function isArray(v): boolean {
|
||||
return Array.isArray(v);
|
||||
}
|
||||
function isEmpty(v): boolean {
|
||||
return (isArray(v) && v.length === 0) || (isObject(v) && Object.keys(v).length === 0);
|
||||
}
|
||||
|
||||
function isEmpty(v): boolean {
|
||||
return (isArray(v) && v.length === 0) || (isObject(v) && Object.keys(v).length === 0);
|
||||
}
|
||||
|
||||
function collapsable(v): boolean {
|
||||
return (isObject(v) || isArray(v)) && !isEmpty(v);
|
||||
}
|
||||
|
||||
return {
|
||||
number,
|
||||
collapsed,
|
||||
isObject,
|
||||
isArray,
|
||||
isEmpty,
|
||||
collapsable,
|
||||
};
|
||||
},
|
||||
});
|
||||
function collapsable(v): boolean {
|
||||
return (isObject(v) || isArray(v)) && !isEmpty(v);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="zhyxdalp">
|
||||
<div>
|
||||
<XValue :value="value" :collapsed="false"/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -12,9 +12,3 @@ const props = defineProps<{
|
||||
value: Record<string, unknown>;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.zhyxdalp {
|
||||
|
||||
}
|
||||
</style>
|
||||
|
@@ -8,7 +8,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted } from 'vue';
|
||||
import { onMounted, onUnmounted } from 'vue';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
@@ -21,16 +21,22 @@ let content = $shallowRef<HTMLElement>();
|
||||
let omitted = $ref(false);
|
||||
let ignoreOmit = $ref(false);
|
||||
|
||||
onMounted(() => {
|
||||
const calcOmit = () => {
|
||||
if (omitted || ignoreOmit) return;
|
||||
omitted = content.offsetHeight > props.maxHeight;
|
||||
};
|
||||
const calcOmit = () => {
|
||||
if (omitted || ignoreOmit) return;
|
||||
omitted = content.offsetHeight > props.maxHeight;
|
||||
};
|
||||
|
||||
const omitObserver = new ResizeObserver((entries, observer) => {
|
||||
calcOmit();
|
||||
new ResizeObserver((entries, observer) => {
|
||||
calcOmit();
|
||||
}).observe(content);
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
calcOmit();
|
||||
omitObserver.observe(content);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
omitObserver.disconnect();
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@@ -1,23 +1,23 @@
|
||||
<template>
|
||||
<MkWindow
|
||||
ref="windowEl"
|
||||
:initial-width="500"
|
||||
:initial-height="500"
|
||||
:can-resize="true"
|
||||
:close-button="true"
|
||||
:buttons-left="buttonsLeft"
|
||||
:buttons-right="buttonsRight"
|
||||
:initialWidth="500"
|
||||
:initialHeight="500"
|
||||
:canResize="true"
|
||||
:closeButton="true"
|
||||
:buttonsLeft="buttonsLeft"
|
||||
:buttonsRight="buttonsRight"
|
||||
:contextmenu="contextmenu"
|
||||
@closed="$emit('closed')"
|
||||
>
|
||||
<template #header>
|
||||
<template v-if="pageMetadata?.value">
|
||||
<i v-if="pageMetadata.value.icon" class="icon" :class="pageMetadata.value.icon" style="margin-right: 0.5em;"></i>
|
||||
<i v-if="pageMetadata.value.icon" :class="pageMetadata.value.icon" style="margin-right: 0.5em;"></i>
|
||||
<span>{{ pageMetadata.value.title }}</span>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<div :class="$style.root" :style="{ background: pageMetadata?.value?.bg }" style="container-type: inline-size;">
|
||||
<div :class="$style.root" style="container-type: inline-size;">
|
||||
<RouterView :key="reloadCount" :router="router"/>
|
||||
</div>
|
||||
</MkWindow>
|
||||
|
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<Transition
|
||||
:enter-active-class="defaultStore.state.animation ? $style.transition_fade_enterActive : ''"
|
||||
:leave-active-class="defaultStore.state.animation ? $style.transition_fade_leaveActive : ''"
|
||||
:enter-from-class="defaultStore.state.animation ? $style.transition_fade_enterFrom : ''"
|
||||
:leave-to-class="defaultStore.state.animation ? $style.transition_fade_leaveTo : ''"
|
||||
:enterActiveClass="defaultStore.state.animation ? $style.transition_fade_enterActive : ''"
|
||||
:leaveActiveClass="defaultStore.state.animation ? $style.transition_fade_leaveActive : ''"
|
||||
:enterFromClass="defaultStore.state.animation ? $style.transition_fade_enterFrom : ''"
|
||||
:leaveToClass="defaultStore.state.animation ? $style.transition_fade_leaveTo : ''"
|
||||
mode="out-in"
|
||||
>
|
||||
<MkLoading v-if="fetching"/>
|
||||
@@ -13,7 +13,7 @@
|
||||
<div v-else-if="empty" key="_empty_" class="empty">
|
||||
<slot name="empty">
|
||||
<div class="_fullinfo">
|
||||
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
|
||||
<img :src="infoImageUrl" class="_ghost"/>
|
||||
<div>{{ i18n.ts.nothing }}</div>
|
||||
</div>
|
||||
</slot>
|
||||
@@ -73,6 +73,8 @@ export type Paging<E extends keyof misskey.Endpoints = keyof misskey.Endpoints>
|
||||
};
|
||||
</script>
|
||||
<script lang="ts" setup>
|
||||
import { infoImageUrl } from '@/instance';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
pagination: Paging;
|
||||
disableAutoLoad?: boolean;
|
||||
|
@@ -1,19 +1,19 @@
|
||||
<template>
|
||||
<div class="tivcixzd" :class="{ done: closed || isVoted }">
|
||||
<ul>
|
||||
<li v-for="(choice, i) in note.poll.choices" :key="i" :class="{ voted: choice.voted }" @click="vote(i)">
|
||||
<div class="backdrop" :style="{ 'width': `${showResult ? (choice.votes / total * 100) : 0}%` }"></div>
|
||||
<span>
|
||||
<template v-if="choice.isVoted"><i class="ti ti-check"></i></template>
|
||||
<div :class="{ [$style.done]: closed || isVoted }">
|
||||
<ul :class="$style.choices">
|
||||
<li v-for="(choice, i) in note.poll.choices" :key="i" :class="$style.choice" @click="vote(i)">
|
||||
<div :class="$style.bg" :style="{ 'width': `${showResult ? (choice.votes / total * 100) : 0}%` }"></div>
|
||||
<span :class="$style.fg">
|
||||
<template v-if="choice.isVoted"><i class="ti ti-check" style="margin-right: 4px; color: var(--accent);"></i></template>
|
||||
<Mfm :text="choice.text" :plain="true"/>
|
||||
<span v-if="showResult" class="votes">({{ i18n.t('_poll.votesCount', { n: choice.votes }) }})</span>
|
||||
<span v-if="showResult" style="margin-left: 4px; opacity: 0.7;">({{ i18n.t('_poll.votesCount', { n: choice.votes }) }})</span>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<p v-if="!readOnly">
|
||||
<p v-if="!readOnly" :class="$style.info">
|
||||
<span>{{ i18n.t('_poll.totalVotes', { n: total }) }}</span>
|
||||
<span> · </span>
|
||||
<a v-if="!closed && !isVoted" @click="showResult = !showResult">{{ showResult ? i18n.ts._poll.vote : i18n.ts._poll.showResult }}</a>
|
||||
<a v-if="!closed && !isVoted" style="color: inherit;" @click="showResult = !showResult">{{ showResult ? i18n.ts._poll.vote : i18n.ts._poll.showResult }}</a>
|
||||
<span v-if="isVoted">{{ i18n.ts._poll.voted }}</span>
|
||||
<span v-else-if="closed">{{ i18n.ts._poll.closed }}</span>
|
||||
<span v-if="remaining > 0"> · {{ timer }}</span>
|
||||
@@ -86,67 +86,51 @@ const vote = async (id) => {
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tivcixzd {
|
||||
> ul {
|
||||
display: block;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
<style lang="scss" module>
|
||||
.choices {
|
||||
display: block;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
> li {
|
||||
display: block;
|
||||
position: relative;
|
||||
margin: 4px 0;
|
||||
padding: 4px;
|
||||
//border: solid 0.5px var(--divider);
|
||||
background: var(--accentedBg);
|
||||
border-radius: 4px;
|
||||
overflow: clip;
|
||||
cursor: pointer;
|
||||
.choice {
|
||||
display: block;
|
||||
position: relative;
|
||||
margin: 4px 0;
|
||||
padding: 4px;
|
||||
//border: solid 0.5px var(--divider);
|
||||
background: var(--accentedBg);
|
||||
border-radius: 4px;
|
||||
overflow: clip;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
> .backdrop {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
background: var(--accent);
|
||||
background: linear-gradient(90deg,var(--buttonGradateA),var(--buttonGradateB));
|
||||
transition: width 1s ease;
|
||||
}
|
||||
.bg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
background: var(--accent);
|
||||
background: linear-gradient(90deg,var(--buttonGradateA),var(--buttonGradateB));
|
||||
transition: width 1s ease;
|
||||
}
|
||||
|
||||
> span {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
padding: 3px 5px;
|
||||
background: var(--panel);
|
||||
border-radius: 3px;
|
||||
.fg {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
padding: 3px 5px;
|
||||
background: var(--panel);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
> i {
|
||||
margin-right: 4px;
|
||||
color: var(--accent);
|
||||
}
|
||||
.info {
|
||||
color: var(--fg);
|
||||
}
|
||||
|
||||
> .votes {
|
||||
margin-left: 4px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> p {
|
||||
color: var(--fg);
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
&.done {
|
||||
> ul > li {
|
||||
cursor: default;
|
||||
}
|
||||
.done {
|
||||
.choice {
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -5,7 +5,7 @@
|
||||
</p>
|
||||
<ul>
|
||||
<li v-for="(choice, i) in choices" :key="i">
|
||||
<MkInput class="input" small :model-value="choice" :placeholder="i18n.t('_poll.choiceN', { n: i + 1 })" @update:model-value="onInput(i, $event)">
|
||||
<MkInput class="input" small :modelValue="choice" :placeholder="i18n.t('_poll.choiceN', { n: i + 1 })" @update:modelValue="onInput(i, $event)">
|
||||
</MkInput>
|
||||
<button class="_button" @click="remove(i)">
|
||||
<i class="ti ti-x"></i>
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<MkModal ref="modal" v-slot="{ type, maxHeight }" :z-priority="'high'" :src="src" :transparent-bg="true" @click="modal.close()" @close="emit('closing')" @closed="emit('closed')">
|
||||
<MkMenu :items="items" :align="align" :width="width" :max-height="maxHeight" :as-drawer="type === 'drawer'" :class="{ [$style.drawer]: type === 'drawer' }" @close="modal.close()"/>
|
||||
<MkModal ref="modal" v-slot="{ type, maxHeight }" :zPriority="'high'" :src="src" :transparentBg="true" @click="modal.close()" @close="emit('closing')" @closed="emit('closed')">
|
||||
<MkMenu :items="items" :align="align" :width="width" :max-height="maxHeight" :asDrawer="type === 'drawer'" :class="{ [$style.drawer]: type === 'drawer' }" @close="modal.close()"/>
|
||||
</MkModal>
|
||||
</template>
|
||||
|
||||
|
@@ -22,21 +22,21 @@
|
||||
<span v-if="visibility === 'specified'"><i class="ti ti-mail"></i></span>
|
||||
<span :class="$style.headerRightButtonText">{{ i18n.ts._visibility[visibility] }}</span>
|
||||
</button>
|
||||
<button v-else :class="['_button', $style.headerRightItem, $style.visibility]" disabled>
|
||||
<button v-else class="_button" :class="[$style.headerRightItem, $style.visibility]" disabled>
|
||||
<span><i class="ti ti-device-tv"></i></span>
|
||||
<span :class="$style.headerRightButtonText">{{ channel.name }}</span>
|
||||
</button>
|
||||
</template>
|
||||
<button v-click-anime v-tooltip="i18n.ts._visibility.disableFederation" :class="['_button', $style.headerRightItem, $style.localOnly, { [$style.danger]: localOnly }]" :disabled="channel != null || visibility === 'specified'" @click="toggleLocalOnly">
|
||||
<button v-click-anime v-tooltip="i18n.ts._visibility.disableFederation" class="_button" :class="[$style.headerRightItem, { [$style.danger]: localOnly }]" :disabled="channel != null || visibility === 'specified'" @click="toggleLocalOnly">
|
||||
<span v-if="!localOnly"><i class="ti ti-rocket"></i></span>
|
||||
<span v-else><i class="ti ti-rocket-off"></i></span>
|
||||
</button>
|
||||
<button v-click-anime v-tooltip="i18n.ts.reactionAcceptance" :class="['_button', $style.headerRightItem, $style.reactionAcceptance, { [$style.danger]: reactionAcceptance }]" @click="toggleReactionAcceptance">
|
||||
<button v-click-anime v-tooltip="i18n.ts.reactionAcceptance" class="_button" :class="[$style.headerRightItem, { [$style.danger]: reactionAcceptance === 'likeOnly' }]" @click="toggleReactionAcceptance">
|
||||
<span v-if="reactionAcceptance === 'likeOnly'"><i class="ti ti-heart"></i></span>
|
||||
<span v-else-if="reactionAcceptance === 'likeOnlyForRemote'"><i class="ti ti-heart-plus"></i></span>
|
||||
<span v-else><i class="ti ti-icons"></i></span>
|
||||
</button>
|
||||
<button v-click-anime class="_button" :class="[$style.submit, { [$style.submitPosting]: posting }]" :disabled="!canPost" data-cy-open-post-form-submit @click="post">
|
||||
<button v-click-anime class="_button" :class="$style.submit" :disabled="!canPost" data-cy-open-post-form-submit @click="post">
|
||||
<div :class="$style.submitInner">
|
||||
<template v-if="posted"></template>
|
||||
<template v-else-if="posting"><MkEllipsis/></template>
|
||||
@@ -66,7 +66,7 @@
|
||||
<div v-if="maxTextLength - textLength < 100" :class="['_acrylic', $style.textCount, { [$style.textOver]: textLength > maxTextLength }]">{{ maxTextLength - textLength }}</div>
|
||||
</div>
|
||||
<input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" :class="$style.hashtags" :placeholder="i18n.ts.hashtags" list="hashtags">
|
||||
<XPostFormAttaches v-model="files" :class="$style.attaches" @detach="detachFile" @change-sensitive="updateFileSensitive" @change-name="updateFileName"/>
|
||||
<XPostFormAttaches v-model="files" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName"/>
|
||||
<MkPollEditor v-if="poll" v-model="poll" @destroyed="poll = null"/>
|
||||
<MkNotePreview v-if="showPreview" :class="$style.preview" :text="text"/>
|
||||
<div v-if="showingOptions" style="padding: 8px 16px;">
|
||||
@@ -484,8 +484,10 @@ async function toggleReactionAcceptance() {
|
||||
title: i18n.ts.reactionAcceptance,
|
||||
items: [
|
||||
{ value: null, text: i18n.ts.all },
|
||||
{ value: 'likeOnly' as const, text: i18n.ts.likeOnly },
|
||||
{ value: 'likeOnlyForRemote' as const, text: i18n.ts.likeOnlyForRemote },
|
||||
{ value: 'nonSensitiveOnly' as const, text: i18n.ts.nonSensitiveOnly },
|
||||
{ value: 'nonSensitiveOnlyForLocalLikeOnlyForRemote' as const, text: i18n.ts.nonSensitiveOnlyForLocalLikeOnlyForRemote },
|
||||
{ value: 'likeOnly' as const, text: i18n.ts.likeOnly },
|
||||
],
|
||||
default: reactionAcceptance,
|
||||
});
|
||||
|
@@ -1,16 +1,16 @@
|
||||
<template>
|
||||
<div v-show="props.modelValue.length != 0" class="skeikyzd">
|
||||
<Sortable :model-value="props.modelValue" class="files" item-key="id" :animation="150" :delay="100" :delay-on-touch-only="true" @update:model-value="v => emit('update:modelValue', v)">
|
||||
<div v-show="props.modelValue.length != 0" :class="$style.root">
|
||||
<Sortable :modelValue="props.modelValue" :class="$style.files" itemKey="id" :animation="150" :delay="100" :delayOnTouchOnly="true" @update:modelValue="v => emit('update:modelValue', v)">
|
||||
<template #item="{element}">
|
||||
<div class="file" @click="showFileMenu(element, $event)" @contextmenu.prevent="showFileMenu(element, $event)">
|
||||
<MkDriveFileThumbnail :data-id="element.id" class="thumbnail" :file="element" fit="cover"/>
|
||||
<div v-if="element.isSensitive" class="sensitive">
|
||||
<i class="ti ti-alert-triangle icon"></i>
|
||||
<div :class="$style.file" @click="showFileMenu(element, $event)" @contextmenu.prevent="showFileMenu(element, $event)">
|
||||
<MkDriveFileThumbnail :data-id="element.id" :class="$style.thumbnail" :file="element" fit="cover"/>
|
||||
<div v-if="element.isSensitive" :class="$style.sensitive">
|
||||
<i class="ti ti-alert-triangle" style="margin: auto;"></i>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Sortable>
|
||||
<p class="remain">{{ 16 - props.modelValue.length }}/16</p>
|
||||
<p :class="$style.remain">{{ 16 - props.modelValue.length }}/16</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -93,7 +93,7 @@ function showFileMenu(file, ev: MouseEvent) {
|
||||
action: () => { rename(file); },
|
||||
}, {
|
||||
text: file.isSensitive ? i18n.ts.unmarkAsSensitive : i18n.ts.markAsSensitive,
|
||||
icon: file.isSensitive ? 'ti ti-eye-off' : 'ti ti-eye',
|
||||
icon: file.isSensitive ? 'ti ti-eye-exclamation' : 'ti ti-eye',
|
||||
action: () => { toggleSensitive(file); },
|
||||
}, {
|
||||
text: i18n.ts.describeFile,
|
||||
@@ -108,60 +108,53 @@ function showFileMenu(file, ev: MouseEvent) {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.skeikyzd {
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
padding: 8px 16px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
> .files {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
.files {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
> .file {
|
||||
position: relative;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin-right: 4px;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
cursor: move;
|
||||
.file {
|
||||
position: relative;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin-right: 4px;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
&:hover > .remove {
|
||||
display: block;
|
||||
}
|
||||
.thumbnail {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
color: var(--fg);
|
||||
}
|
||||
|
||||
> .thumbnail {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
color: var(--fg);
|
||||
}
|
||||
.sensitive {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
background: rgba(17, 17, 17, .7);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
> .sensitive {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
background: rgba(17, 17, 17, .7);
|
||||
color: #fff;
|
||||
|
||||
> .icon {
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .remain {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.remain {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 90%;
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<MkModal ref="modal" :prefer-type="'dialog'" @click="modal.close()" @closed="onModalClosed()">
|
||||
<MkPostForm ref="form" style="margin: 0 auto auto auto;" v-bind="props" autofocus freeze-after-posted @posted="onPosted" @cancel="modal.close()" @esc="modal.close()"/>
|
||||
<MkModal ref="modal" :preferType="'dialog'" @click="modal.close()" @closed="onModalClosed()">
|
||||
<MkPostForm ref="form" style="margin: 0 auto auto auto;" v-bind="props" autofocus freezeAfterPosted @posted="onPosted" @cancel="modal.close()" @esc="modal.close()"/>
|
||||
</MkModal>
|
||||
</template>
|
||||
|
||||
|
@@ -72,28 +72,28 @@ function subscribe() {
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: urlBase64ToUint8Array(instance.swPublickey),
|
||||
})
|
||||
.then(async subscription => {
|
||||
pushSubscription = subscription;
|
||||
.then(async subscription => {
|
||||
pushSubscription = subscription;
|
||||
|
||||
// Register
|
||||
pushRegistrationInServer = await api('sw/register', {
|
||||
endpoint: subscription.endpoint,
|
||||
auth: encode(subscription.getKey('auth')),
|
||||
publickey: encode(subscription.getKey('p256dh')),
|
||||
});
|
||||
}, async err => { // When subscribe failed
|
||||
// Register
|
||||
pushRegistrationInServer = await api('sw/register', {
|
||||
endpoint: subscription.endpoint,
|
||||
auth: encode(subscription.getKey('auth')),
|
||||
publickey: encode(subscription.getKey('p256dh')),
|
||||
});
|
||||
}, async err => { // When subscribe failed
|
||||
// 通知が許可されていなかったとき
|
||||
if (err?.name === 'NotAllowedError') {
|
||||
console.info('User denied the notification permission request.');
|
||||
return;
|
||||
}
|
||||
if (err?.name === 'NotAllowedError') {
|
||||
console.info('User denied the notification permission request.');
|
||||
return;
|
||||
}
|
||||
|
||||
// 違うapplicationServerKey (または gcm_sender_id)のサブスクリプションが
|
||||
// 既に存在していることが原因でエラーになった可能性があるので、
|
||||
// そのサブスクリプションを解除しておく
|
||||
// (これは実行されなさそうだけど、おまじない的に古い実装から残してある)
|
||||
await unsubscribe();
|
||||
}), null, null);
|
||||
// 違うapplicationServerKey (または gcm_sender_id)のサブスクリプションが
|
||||
// 既に存在していることが原因でエラーになった可能性があるので、
|
||||
// そのサブスクリプションを解除しておく
|
||||
// (これは実行されなさそうだけど、おまじない的に古い実装から残してある)
|
||||
await unsubscribe();
|
||||
}), null, null);
|
||||
}
|
||||
|
||||
async function unsubscribe() {
|
||||
|
@@ -1,37 +1,27 @@
|
||||
<script lang="ts">
|
||||
import { VNode, defineComponent, h } from 'vue';
|
||||
import { VNode, defineComponent, h, ref, watch } from 'vue';
|
||||
import MkRadio from './MkRadio.vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkRadio,
|
||||
},
|
||||
props: {
|
||||
modelValue: {
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
value: this.modelValue,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
value() {
|
||||
this.$emit('update:modelValue', this.value);
|
||||
},
|
||||
},
|
||||
render() {
|
||||
console.log(this.$slots, this.$slots.label && this.$slots.label());
|
||||
if (!this.$slots.default) return null;
|
||||
let options = this.$slots.default();
|
||||
const label = this.$slots.label && this.$slots.label();
|
||||
const caption = this.$slots.caption && this.$slots.caption();
|
||||
setup(props, context) {
|
||||
const value = ref(props.modelValue);
|
||||
watch(value, () => {
|
||||
context.emit('update:modelValue', value.value);
|
||||
});
|
||||
if (!context.slots.default) return null;
|
||||
let options = context.slots.default();
|
||||
const label = context.slots.label && context.slots.label();
|
||||
const caption = context.slots.caption && context.slots.caption();
|
||||
|
||||
// なぜかFragmentになることがあるため
|
||||
if (options.length === 1 && options[0].props == null) options = options[0].children as VNode[];
|
||||
|
||||
return h('div', {
|
||||
return () => h('div', {
|
||||
class: 'novjtcto',
|
||||
}, [
|
||||
...(label ? [h('div', {
|
||||
@@ -42,8 +32,8 @@ export default defineComponent({
|
||||
}, options.map(option => h(MkRadio, {
|
||||
key: option.key,
|
||||
value: option.props?.value,
|
||||
modelValue: this.value,
|
||||
'onUpdate:modelValue': value => this.value = value,
|
||||
modelValue: value.value,
|
||||
'onUpdate:modelValue': _v => value.value = _v,
|
||||
}, () => option.children)),
|
||||
),
|
||||
...(caption ? [h('div', {
|
||||
|
@@ -8,10 +8,10 @@
|
||||
>
|
||||
<template #header>{{ i18n.ts.reactionsList }}</template>
|
||||
|
||||
<MkSpacer :margin-min="20" :margin-max="28">
|
||||
<MkSpacer :marginMin="20" :marginMax="28">
|
||||
<div v-if="note" class="_gaps">
|
||||
<div v-if="reactions.length === 0" class="_fullinfo">
|
||||
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
|
||||
<img :src="infoImageUrl" class="_ghost"/>
|
||||
<div>{{ i18n.ts.nothing }}</div>
|
||||
</div>
|
||||
<template v-else>
|
||||
@@ -22,7 +22,7 @@
|
||||
</button>
|
||||
</div>
|
||||
<MkA v-for="user in users" :key="user.id" :to="userPage(user)" @click="dialog.close()">
|
||||
<MkUserCardMini :user="user" :with-chart="false"/>
|
||||
<MkUserCardMini :user="user" :withChart="false"/>
|
||||
</MkA>
|
||||
</template>
|
||||
</div>
|
||||
@@ -42,6 +42,7 @@ import MkUserCardMini from '@/components/MkUserCardMini.vue';
|
||||
import { userPage } from '@/filters/user';
|
||||
import { i18n } from '@/i18n';
|
||||
import * as os from '@/os';
|
||||
import { infoImageUrl } from '@/instance';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'closed'): void,
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<MkCustomEmoji v-if="reaction[0] === ':'" :name="reaction" :normal="true" :no-style="noStyle" :url="emojiUrl"/>
|
||||
<MkEmoji v-else :emoji="reaction" :normal="true" :no-style="noStyle"/>
|
||||
<MkCustomEmoji v-if="reaction[0] === ':'" :name="reaction" :normal="true" :noStyle="noStyle" :url="emojiUrl"/>
|
||||
<MkEmoji v-else :emoji="reaction" :normal="true" :noStyle="noStyle"/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<MkTooltip ref="tooltip" :showing="showing" :target-element="targetElement" :max-width="340" @closed="emit('closed')">
|
||||
<MkTooltip ref="tooltip" :showing="showing" :targetElement="targetElement" :maxWidth="340" @closed="emit('closed')">
|
||||
<div :class="$style.root">
|
||||
<MkReactionIcon :reaction="reaction" :class="$style.icon" :no-style="true"/>
|
||||
<MkReactionIcon :reaction="reaction" :class="$style.icon" :noStyle="true"/>
|
||||
<div :class="$style.name">{{ reaction.replace('@.', '') }}</div>
|
||||
</div>
|
||||
</MkTooltip>
|
||||
|
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<MkTooltip ref="tooltip" :showing="showing" :target-element="targetElement" :max-width="340" @closed="emit('closed')">
|
||||
<MkTooltip ref="tooltip" :showing="showing" :targetElement="targetElement" :maxWidth="340" @closed="emit('closed')">
|
||||
<div :class="$style.root">
|
||||
<div :class="$style.reaction">
|
||||
<MkReactionIcon :reaction="reaction" :class="$style.reactionIcon" :no-style="true"/>
|
||||
<MkReactionIcon :reaction="reaction" :class="$style.reactionIcon" :noStyle="true"/>
|
||||
<div :class="$style.reactionName">{{ getReactionName(reaction) }}</div>
|
||||
</div>
|
||||
<div :class="$style.users">
|
||||
|
@@ -6,7 +6,7 @@
|
||||
:class="[$style.root, { [$style.reacted]: note.myReaction == reaction, [$style.canToggle]: canToggle, [$style.large]: defaultStore.state.largeNoteReactions }]"
|
||||
@click="toggleReaction()"
|
||||
>
|
||||
<MkReactionIcon :class="$style.icon" :reaction="reaction" :emoji-url="note.reactionEmojis[reaction.substr(1, reaction.length - 2)]"/>
|
||||
<MkReactionIcon :class="$style.icon" :reaction="reaction" :emojiUrl="note.reactionEmojis[reaction.substr(1, reaction.length - 2)]"/>
|
||||
<span :class="$style.count">{{ count }}</span>
|
||||
</button>
|
||||
</template>
|
||||
@@ -22,6 +22,7 @@ import { $i } from '@/account';
|
||||
import MkReactionEffect from '@/components/MkReactionEffect.vue';
|
||||
import { claimAchievement } from '@/scripts/achievements';
|
||||
import { defaultStore } from '@/store';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
const props = defineProps<{
|
||||
reaction: string;
|
||||
@@ -34,11 +35,19 @@ const buttonEl = shallowRef<HTMLElement>();
|
||||
|
||||
const canToggle = computed(() => !props.reaction.match(/@\w/) && $i);
|
||||
|
||||
const toggleReaction = () => {
|
||||
async function toggleReaction() {
|
||||
if (!canToggle.value) return;
|
||||
|
||||
// TODO: その絵文字を使う権限があるかどうか確認
|
||||
|
||||
const oldReaction = props.note.myReaction;
|
||||
if (oldReaction) {
|
||||
const confirm = await os.confirm({
|
||||
type: 'warning',
|
||||
text: oldReaction !== props.reaction ? i18n.ts.changeReactionConfirm : i18n.ts.cancelReactionConfirm,
|
||||
});
|
||||
if (confirm.canceled) return;
|
||||
|
||||
os.api('notes/reactions/delete', {
|
||||
noteId: props.note.id,
|
||||
}).then(() => {
|
||||
@@ -58,9 +67,9 @@ const toggleReaction = () => {
|
||||
claimAchievement('reactWithoutRead');
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const anime = () => {
|
||||
function anime() {
|
||||
if (document.hidden) return;
|
||||
if (!defaultStore.state.animation) return;
|
||||
|
||||
@@ -68,7 +77,7 @@ const anime = () => {
|
||||
const x = rect.left + 16;
|
||||
const y = rect.top + (buttonEl.value.offsetHeight / 2);
|
||||
os.popup(MkReactionEffect, { reaction: props.reaction, x, y }, {}, 'end');
|
||||
};
|
||||
}
|
||||
|
||||
watch(() => props.count, (newCount, oldCount) => {
|
||||
if (oldCount < newCount) anime();
|
||||
|
@@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<TransitionGroup
|
||||
:enter-active-class="defaultStore.state.animation ? $style.transition_x_enterActive : ''"
|
||||
:leave-active-class="defaultStore.state.animation ? $style.transition_x_leaveActive : ''"
|
||||
:enter-from-class="defaultStore.state.animation ? $style.transition_x_enterFrom : ''"
|
||||
:leave-to-class="defaultStore.state.animation ? $style.transition_x_leaveTo : ''"
|
||||
:move-class="defaultStore.state.animation ? $style.transition_x_move : ''"
|
||||
:enterActiveClass="defaultStore.state.animation ? $style.transition_x_enterActive : ''"
|
||||
:leaveActiveClass="defaultStore.state.animation ? $style.transition_x_leaveActive : ''"
|
||||
:enterFromClass="defaultStore.state.animation ? $style.transition_x_enterFrom : ''"
|
||||
:leaveToClass="defaultStore.state.animation ? $style.transition_x_leaveTo : ''"
|
||||
:moveClass="defaultStore.state.animation ? $style.transition_x_move : ''"
|
||||
tag="div" :class="$style.root"
|
||||
>
|
||||
<XReaction v-for="[reaction, count] in reactions" :key="reaction" :reaction="reaction" :count="count" :is-initial="initialReactions.has(reaction)" :note="note"/>
|
||||
<XReaction v-for="[reaction, count] in reactions" :key="reaction" :reaction="reaction" :count="count" :isInitial="initialReactions.has(reaction)" :note="note"/>
|
||||
<slot v-if="hasMoreReactions" name="more"/>
|
||||
</TransitionGroup>
|
||||
</template>
|
||||
|
@@ -8,15 +8,15 @@
|
||||
>
|
||||
<template #header>{{ i18n.ts.renotesList }}</template>
|
||||
|
||||
<MkSpacer :margin-min="20" :margin-max="28">
|
||||
<MkSpacer :marginMin="20" :marginMax="28">
|
||||
<div v-if="renotes" class="_gaps">
|
||||
<div v-if="renotes.length === 0" class="_fullinfo">
|
||||
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
|
||||
<img :src="infoImageUrl" class="_ghost"/>
|
||||
<div>{{ i18n.ts.nothing }}</div>
|
||||
</div>
|
||||
<template v-else>
|
||||
<MkA v-for="user in users" :key="user.id" :to="userPage(user)" @click="dialog.close()">
|
||||
<MkUserCardMini :user="user" :with-chart="false"/>
|
||||
<MkUserCardMini :user="user" :withChart="false"/>
|
||||
</MkA>
|
||||
</template>
|
||||
</div>
|
||||
@@ -35,6 +35,7 @@ import MkUserCardMini from '@/components/MkUserCardMini.vue';
|
||||
import { userPage } from '@/filters/user';
|
||||
import { i18n } from '@/i18n';
|
||||
import * as os from '@/os';
|
||||
import { infoImageUrl } from '@/instance';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'closed'): void,
|
||||
|
@@ -124,7 +124,3 @@ onMounted(async () => {
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
|
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="vswabwbm" :style="{ zIndex, top: `${y - 64}px`, left: `${x - 64}px` }">
|
||||
<div :class="$style.root" :style="{ zIndex, top: `${y - 64}px`, left: `${x - 64}px` }">
|
||||
<svg width="128" height="128" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle fill="none" cx="64" cy="64">
|
||||
<circle fill="none" cx="64" cy="64" style="stroke: var(--accent);">
|
||||
<animate
|
||||
attributeName="r"
|
||||
begin="0s" dur="0.5s"
|
||||
@@ -22,7 +22,7 @@
|
||||
/>
|
||||
</circle>
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<circle v-for="(particle, i) in particles" :key="i" :fill="particle.color">
|
||||
<circle v-for="(particle, i) in particles" :key="i" :fill="particle.color" style="stroke: var(--accent);">
|
||||
<animate
|
||||
attributeName="r"
|
||||
begin="0s" dur="0.8s"
|
||||
@@ -100,17 +100,11 @@ onMounted(() => {
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.vswabwbm {
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
pointer-events: none;
|
||||
position: fixed;
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
|
||||
> svg {
|
||||
> circle {
|
||||
stroke: var(--accent);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -12,8 +12,10 @@
|
||||
</template>
|
||||
</span>
|
||||
<span :class="$style.name">{{ role.name }}</span>
|
||||
<span v-if="role.target === 'manual'" :class="$style.users">{{ role.usersCount }} users</span>
|
||||
<span v-else-if="role.target === 'conditional'" :class="$style.users">({{ i18n.ts._role.conditional }})</span>
|
||||
<template v-if="detailed">
|
||||
<span v-if="role.target === 'manual'" :class="$style.users">{{ role.usersCount }} users</span>
|
||||
<span v-else-if="role.target === 'conditional'" :class="$style.users">({{ i18n.ts._role.conditional }})</span>
|
||||
</template>
|
||||
</div>
|
||||
<div :class="$style.description">{{ role.description }}</div>
|
||||
</MkA>
|
||||
@@ -23,10 +25,13 @@
|
||||
import { } from 'vue';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
const props = defineProps<{
|
||||
const props = withDefaults(defineProps<{
|
||||
role: any;
|
||||
forModeration: boolean;
|
||||
}>();
|
||||
detailed: boolean;
|
||||
}>(), {
|
||||
detailed: true,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
@@ -1,118 +0,0 @@
|
||||
<template>
|
||||
<div class="">
|
||||
<div class="">
|
||||
<MkInput v-model="text">
|
||||
<template #label>Text</template>
|
||||
</MkInput>
|
||||
<MkSwitch v-model="flag">
|
||||
<span>Switch is now {{ flag ? 'on' : 'off' }}</span>
|
||||
</MkSwitch>
|
||||
<div style="margin: 32px 0;">
|
||||
<MkRadio v-model="radio" value="misskey">Misskey</MkRadio>
|
||||
<MkRadio v-model="radio" value="mastodon">Mastodon</MkRadio>
|
||||
<MkRadio v-model="radio" value="pleroma">Pleroma</MkRadio>
|
||||
</div>
|
||||
<MkButton inline>This is</MkButton>
|
||||
<MkButton inline primary>the button</MkButton>
|
||||
</div>
|
||||
<div class="" style="pointer-events: none;">
|
||||
<Mfm :text="mfm"/>
|
||||
</div>
|
||||
<div class="">
|
||||
<MkButton inline primary @click="openMenu">Open menu</MkButton>
|
||||
<MkButton inline primary @click="openDialog">Open dialog</MkButton>
|
||||
<MkButton inline primary @click="openForm">Open form</MkButton>
|
||||
<MkButton inline primary @click="openDrive">Open drive</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
import MkRadio from '@/components/MkRadio.vue';
|
||||
import * as os from '@/os';
|
||||
import * as config from '@/config';
|
||||
import { $i } from '@/account';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkButton,
|
||||
MkInput,
|
||||
MkSwitch,
|
||||
MkTextarea,
|
||||
MkRadio,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
text: '',
|
||||
flag: true,
|
||||
radio: 'misskey',
|
||||
$i,
|
||||
mfm: `Hello world! This is an @example mention. BTW you are @${this.$i ? this.$i.username : 'guest'}.\nAlso, here is ${config.url} and [example link](${config.url}). for more details, see https://example.com.\nAs you know #misskey is open-source software.`,
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
async openDialog() {
|
||||
os.alert({
|
||||
type: 'warning',
|
||||
title: 'Oh my Aichan',
|
||||
text: 'Lorem ipsum dolor sit amet, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
|
||||
});
|
||||
},
|
||||
|
||||
async openForm() {
|
||||
os.form('Example form', {
|
||||
foo: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
label: 'This is a boolean property',
|
||||
},
|
||||
bar: {
|
||||
type: 'number',
|
||||
default: 300,
|
||||
label: 'This is a number property',
|
||||
},
|
||||
baz: {
|
||||
type: 'string',
|
||||
default: 'Misskey makes you happy.',
|
||||
label: 'This is a string property',
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
async openDrive() {
|
||||
os.selectDriveFile(false);
|
||||
},
|
||||
|
||||
async selectUser() {
|
||||
os.selectUser();
|
||||
},
|
||||
|
||||
async openMenu(ev) {
|
||||
os.popupMenu([{
|
||||
type: 'label',
|
||||
text: 'Fruits',
|
||||
}, {
|
||||
text: 'Create some apples',
|
||||
action: () => {},
|
||||
}, {
|
||||
text: 'Read some oranges',
|
||||
action: () => {},
|
||||
}, {
|
||||
text: 'Update some melons',
|
||||
action: () => {},
|
||||
}, null, {
|
||||
text: 'Delete some bananas',
|
||||
danger: true,
|
||||
action: () => {},
|
||||
}], ev.currentTarget ?? ev.target);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
@@ -1,16 +1,16 @@
|
||||
<template>
|
||||
<form class="eppvobhk" :class="{ signing, totpLogin }" @submit.prevent="onSubmit">
|
||||
<div class="auth _gaps_m">
|
||||
<div v-show="withAvatar" class="avatar" :style="{ backgroundImage: user ? `url('${ user.avatarUrl }')` : null, marginBottom: message ? '1.5em' : null }"></div>
|
||||
<form :class="{ signing, totpLogin }" @submit.prevent="onSubmit">
|
||||
<div class="_gaps_m">
|
||||
<div v-show="withAvatar" :class="$style.avatar" :style="{ backgroundImage: user ? `url('${ user.avatarUrl }')` : null, marginBottom: message ? '1.5em' : null }"></div>
|
||||
<MkInfo v-if="message">
|
||||
{{ message }}
|
||||
</MkInfo>
|
||||
<div v-if="!totpLogin" class="normal-signin _gaps_m">
|
||||
<MkInput v-model="username" :placeholder="i18n.ts.username" type="text" pattern="^[a-zA-Z0-9_]+$" :spellcheck="false" autocomplete="username" autofocus required data-cy-signin-username @update:model-value="onUsernameChange">
|
||||
<MkInput v-model="username" :placeholder="i18n.ts.username" type="text" pattern="^[a-zA-Z0-9_]+$" :spellcheck="false" autocomplete="username" autofocus required data-cy-signin-username @update:modelValue="onUsernameChange">
|
||||
<template #prefix>@</template>
|
||||
<template #suffix>@{{ host }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-if="!user || user && !user.usePasswordLessLogin" v-model="password" :placeholder="i18n.ts.password" type="password" autocomplete="current-password" :with-password-toggle="true" required data-cy-signin-password>
|
||||
<MkInput v-if="!user || user && !user.usePasswordLessLogin" v-model="password" :placeholder="i18n.ts.password" type="password" autocomplete="current-password" :withPasswordToggle="true" required data-cy-signin-password>
|
||||
<template #prefix><i class="ti ti-lock"></i></template>
|
||||
<template #caption><button class="_textButton" type="button" @click="resetPassword">{{ i18n.ts.forgotPassword }}</button></template>
|
||||
</MkInput>
|
||||
@@ -28,7 +28,7 @@
|
||||
</div>
|
||||
<div class="twofa-group totp-group">
|
||||
<p style="margin-bottom:0;">{{ i18n.ts.twoStepAuthentication }}</p>
|
||||
<MkInput v-if="user && user.usePasswordLessLogin" v-model="password" type="password" autocomplete="current-password" :with-password-toggle="true" required>
|
||||
<MkInput v-if="user && user.usePasswordLessLogin" v-model="password" type="password" autocomplete="current-password" :withPasswordToggle="true" required>
|
||||
<template #label>{{ i18n.ts.password }}</template>
|
||||
<template #prefix><i class="ti ti-lock"></i></template>
|
||||
</MkInput>
|
||||
@@ -236,18 +236,14 @@ function resetPassword() {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.eppvobhk {
|
||||
> .auth {
|
||||
> .avatar {
|
||||
margin: 0 auto 0 auto;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
background: #ddd;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
border-radius: 100%;
|
||||
}
|
||||
}
|
||||
<style lang="scss" module>
|
||||
.avatar {
|
||||
margin: 0 auto 0 auto;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
background: #ddd;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
border-radius: 100%;
|
||||
}
|
||||
</style>
|
||||
|
@@ -8,8 +8,8 @@
|
||||
>
|
||||
<template #header>{{ i18n.ts.login }}</template>
|
||||
|
||||
<MkSpacer :margin-min="20" :margin-max="28">
|
||||
<MkSignin :auto-set="autoSet" :message="message" @login="onLogin"/>
|
||||
<MkSpacer :marginMin="20" :marginMax="28">
|
||||
<MkSignin :autoSet="autoSet" :message="message" @login="onLogin"/>
|
||||
</MkSpacer>
|
||||
</MkModalWindow>
|
||||
</template>
|
||||
|
@@ -3,13 +3,13 @@
|
||||
<div :class="$style.banner">
|
||||
<i class="ti ti-user-edit"></i>
|
||||
</div>
|
||||
<MkSpacer :margin-min="20" :margin-max="32">
|
||||
<MkSpacer :marginMin="20" :marginMax="32">
|
||||
<form class="_gaps_m" autocomplete="new-password" @submit.prevent="onSubmit">
|
||||
<MkInput v-if="instance.disableRegistration" v-model="invitationCode" type="text" :spellcheck="false" required>
|
||||
<template #label>{{ i18n.ts.invitationCode }}</template>
|
||||
<template #prefix><i class="ti ti-key"></i></template>
|
||||
</MkInput>
|
||||
<MkInput v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :spellcheck="false" required data-cy-signup-username @update:model-value="onChangeUsername">
|
||||
<MkInput v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :spellcheck="false" autocomplete="username" required data-cy-signup-username @update:modelValue="onChangeUsername">
|
||||
<template #label>{{ i18n.ts.username }} <div v-tooltip:dialog="i18n.ts.usernameInfo" class="_button _help"><i class="ti ti-help-circle"></i></div></template>
|
||||
<template #prefix>@</template>
|
||||
<template #suffix>@{{ host }}</template>
|
||||
@@ -24,7 +24,7 @@
|
||||
<span v-else-if="usernameState === 'max-range'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.tooLong }}</span>
|
||||
</template>
|
||||
</MkInput>
|
||||
<MkInput v-if="instance.emailRequiredForSignup" v-model="email" :debounce="true" type="email" :spellcheck="false" required data-cy-signup-email @update:model-value="onChangeEmail">
|
||||
<MkInput v-if="instance.emailRequiredForSignup" v-model="email" :debounce="true" type="email" :spellcheck="false" required data-cy-signup-email @update:modelValue="onChangeEmail">
|
||||
<template #label>{{ i18n.ts.emailAddress }} <div v-tooltip:dialog="i18n.ts._signup.emailAddressInfo" class="_button _help"><i class="ti ti-help-circle"></i></div></template>
|
||||
<template #prefix><i class="ti ti-mail"></i></template>
|
||||
<template #caption>
|
||||
@@ -39,7 +39,7 @@
|
||||
<span v-else-if="emailState === 'error'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.error }}</span>
|
||||
</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="password" type="password" autocomplete="new-password" required data-cy-signup-password @update:model-value="onChangePassword">
|
||||
<MkInput v-model="password" type="password" autocomplete="new-password" required data-cy-signup-password @update:modelValue="onChangePassword">
|
||||
<template #label>{{ i18n.ts.password }}</template>
|
||||
<template #prefix><i class="ti ti-lock"></i></template>
|
||||
<template #caption>
|
||||
@@ -48,7 +48,7 @@
|
||||
<span v-if="passwordStrength == 'high'" style="color: var(--success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.strongPassword }}</span>
|
||||
</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="retypedPassword" type="password" autocomplete="new-password" required data-cy-signup-password-retype @update:model-value="onChangePasswordRetype">
|
||||
<MkInput v-model="retypedPassword" type="password" autocomplete="new-password" required data-cy-signup-password-retype @update:modelValue="onChangePasswordRetype">
|
||||
<template #label>{{ i18n.ts.password }} ({{ i18n.ts.retype }})</template>
|
||||
<template #prefix><i class="ti ti-lock"></i></template>
|
||||
<template #caption>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user