Merge tag '2023.12.0-beta.5' into merge-upstream
This commit is contained in:
@@ -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.37.0/tabler-icons.min.css">
|
||||
<link rel="stylesheet" href="https://unpkg.com/@tabler/icons-webfont@2.44.0/tabler-icons.min.css">
|
||||
<link rel="stylesheet" href="https://unpkg.com/@fontsource/m-plus-rounded-1c/index.css">
|
||||
<style>
|
||||
html {
|
||||
|
3
packages/frontend/@types/global.d.ts
vendored
3
packages/frontend/@types/global.d.ts
vendored
@@ -13,3 +13,6 @@ declare const _PERF_PREFIX_: string;
|
||||
declare const _DATA_TRANSFER_DRIVE_FILE_: string;
|
||||
declare const _DATA_TRANSFER_DRIVE_FOLDER_: string;
|
||||
declare const _DATA_TRANSFER_DECK_COLUMN_: string;
|
||||
|
||||
// for dev-mode
|
||||
declare const _LANGS_FULL_: string[][];
|
||||
|
@@ -180,7 +180,7 @@ import './photoswipe-!~{003}~.js';
|
||||
const _hoisted_1 = createBaseVNode("i", {
|
||||
class: "ti ti-photo"
|
||||
}, null, -1);
|
||||
const _sfc_main = defineComponent({
|
||||
const index_photos = defineComponent({
|
||||
__name: "index.photos",
|
||||
props: {
|
||||
user: {}
|
||||
@@ -261,7 +261,6 @@ const style0 = {
|
||||
const cssModules = {
|
||||
"$style": style0
|
||||
};
|
||||
const index_photos = _sfc_main;
|
||||
export {index_photos as default};
|
||||
`.slice(1));
|
||||
});
|
||||
|
@@ -13,13 +13,13 @@ function isFalsyIdentifier(identifier: estree.Identifier): boolean {
|
||||
return identifier.name === 'undefined' || identifier.name === 'NaN';
|
||||
}
|
||||
|
||||
function normalizeClassWalker(tree: estree.Node): string | null {
|
||||
function normalizeClassWalker(tree: estree.Node, stack: string | undefined): 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);
|
||||
const left = normalizeClassWalker(tree.left, stack);
|
||||
const right = normalizeClassWalker(tree.right, stack);
|
||||
if (left === null || right === null) return null;
|
||||
return `${left}${right}`;
|
||||
}
|
||||
@@ -33,15 +33,15 @@ function normalizeClassWalker(tree: estree.Node): string | null {
|
||||
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 (treeNode.type === 'SpreadElement') return normalizeClassWalker(treeNode.argument, stack);
|
||||
return normalizeClassWalker(treeNode, stack);
|
||||
});
|
||||
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);
|
||||
if (treeNode.type === 'SpreadElement') return normalizeClassWalker(treeNode.argument, stack);
|
||||
let x = treeNode.value;
|
||||
let inveted = false;
|
||||
while (x.type === 'UnaryExpression' && x.operator === '!') {
|
||||
@@ -67,18 +67,26 @@ function normalizeClassWalker(tree: estree.Node): string | null {
|
||||
if (values.some((x) => x === null)) return null;
|
||||
return values.join(' ');
|
||||
}
|
||||
console.error(`Unexpected node type: ${tree.type}`);
|
||||
if (
|
||||
tree.type !== 'CallExpression' &&
|
||||
tree.type !== 'ChainExpression' &&
|
||||
tree.type !== 'ConditionalExpression' &&
|
||||
tree.type !== 'LogicalExpression' &&
|
||||
tree.type !== 'MemberExpression') {
|
||||
console.error(stack ? `Unexpected node type: ${tree.type} (in ${stack})` : `Unexpected node type: ${tree.type}`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function normalizeClass(tree: estree.Node): string | null {
|
||||
const walked = normalizeClassWalker(tree);
|
||||
export function normalizeClass(tree: estree.Node, stack?: string): string | null {
|
||||
const walked = normalizeClassWalker(tree, stack);
|
||||
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 {
|
||||
//#region
|
||||
if (parent?.type !== 'Program') return;
|
||||
if (node.type !== 'VariableDeclaration') return;
|
||||
if (node.declarations.length !== 1) return;
|
||||
@@ -102,6 +110,14 @@ export function unwindCssModuleClassName(ast: estree.Node): void {
|
||||
return true;
|
||||
});
|
||||
if (!~__cssModulesIndex) return;
|
||||
/* This region assumeed that the entered node looks like the following code.
|
||||
*
|
||||
* ```ts
|
||||
* const SomeComponent = _export_sfc(_sfc_main, [["foo", bar], ["__cssModules", cssModules]]);
|
||||
* ```
|
||||
*/
|
||||
//#endregion
|
||||
//#region
|
||||
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;
|
||||
@@ -117,6 +133,16 @@ export function unwindCssModuleClassName(ast: estree.Node): void {
|
||||
if (property.value.type !== 'Identifier') return [];
|
||||
return [[property.key.value as string, property.value.name as string]];
|
||||
}));
|
||||
/* This region collected a VariableDeclaration node in the module that looks like the following code.
|
||||
*
|
||||
* ```ts
|
||||
* const cssModules = {
|
||||
* "$style": style0,
|
||||
* };
|
||||
* ```
|
||||
*/
|
||||
//#endregion
|
||||
//#region
|
||||
const sfcMain = parent.body.find((x) => {
|
||||
if (x.type !== 'VariableDeclaration') return false;
|
||||
if (x.declarations.length !== 1) return false;
|
||||
@@ -146,7 +172,22 @@ export function unwindCssModuleClassName(ast: estree.Node): void {
|
||||
if (ctx.type !== 'Identifier') return;
|
||||
if (ctx.name !== '_ctx') return;
|
||||
if (render.argument.body.type !== 'BlockStatement') return;
|
||||
/* This region assumed that `sfcMain` looks like the following code.
|
||||
*
|
||||
* ```ts
|
||||
* const _sfc_main = defineComponent({
|
||||
* setup(_props) {
|
||||
* ...
|
||||
* return (_ctx, _cache) => {
|
||||
* ...
|
||||
* };
|
||||
* },
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
//#endregion
|
||||
for (const [key, value] of moduleForest) {
|
||||
//#region
|
||||
const cssModuleTreeNode = parent.body.find((x) => {
|
||||
if (x.type !== 'VariableDeclaration') return false;
|
||||
if (x.declarations.length !== 1) return false;
|
||||
@@ -172,6 +213,19 @@ export function unwindCssModuleClassName(ast: estree.Node): void {
|
||||
if (actualValue.declarations[0].init?.type !== 'Literal') return [];
|
||||
return [[actualKey, actualValue.declarations[0].init.value as string]];
|
||||
}));
|
||||
/* This region collected VariableDeclaration nodes in the module that looks like the following code.
|
||||
*
|
||||
* ```ts
|
||||
* const foo = "bar";
|
||||
* const baz = "qux";
|
||||
* const style0 = {
|
||||
* foo: foo,
|
||||
* baz: baz,
|
||||
* };
|
||||
* ```
|
||||
*/
|
||||
//#endregion
|
||||
//#region
|
||||
(walk as typeof estreeWalker.walk)(render.argument.body, {
|
||||
enter(childNode) {
|
||||
if (childNode.type !== 'MemberExpression') return;
|
||||
@@ -189,6 +243,39 @@ export function unwindCssModuleClassName(ast: estree.Node): void {
|
||||
});
|
||||
},
|
||||
});
|
||||
/* This region inlined the reference identifier of the class name in the render function into the actual literal, as in the following code.
|
||||
*
|
||||
* ```ts
|
||||
* const _sfc_main = defineComponent({
|
||||
* setup(_props) {
|
||||
* ...
|
||||
* return (_ctx, _cache) => {
|
||||
* ...
|
||||
* return openBlock(), createElementBlock("div", {
|
||||
* class: normalizeClass(_ctx.$style.foo),
|
||||
* }, null);
|
||||
* };
|
||||
* },
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* ↓
|
||||
*
|
||||
* ```ts
|
||||
* const _sfc_main = defineComponent({
|
||||
* setup(_props) {
|
||||
* ...
|
||||
* return (_ctx, _cache) => {
|
||||
* ...
|
||||
* return openBlock(), createElementBlock("div", {
|
||||
* class: normalizeClass("bar"),
|
||||
* }, null);
|
||||
* };
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
//#endregion
|
||||
//#region
|
||||
(walk as typeof estreeWalker.walk)(render.argument.body, {
|
||||
enter(childNode) {
|
||||
if (childNode.type !== 'MemberExpression') return;
|
||||
@@ -205,13 +292,47 @@ export function unwindCssModuleClassName(ast: estree.Node): void {
|
||||
});
|
||||
},
|
||||
});
|
||||
/* This region replaced the reference identifier of missing class names in the render function with `undefined`, as in the following code.
|
||||
*
|
||||
* ```ts
|
||||
* const _sfc_main = defineComponent({
|
||||
* setup(_props) {
|
||||
* ...
|
||||
* return (_ctx, _cache) => {
|
||||
* ...
|
||||
* return openBlock(), createElementBlock("div", {
|
||||
* class: normalizeClass(_ctx.$style.hoge),
|
||||
* }, null);
|
||||
* };
|
||||
* },
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* ↓
|
||||
*
|
||||
* ```ts
|
||||
* const _sfc_main = defineComponent({
|
||||
* setup(_props) {
|
||||
* ...
|
||||
* return (_ctx, _cache) => {
|
||||
* ...
|
||||
* return openBlock(), createElementBlock("div", {
|
||||
* class: normalizeClass(undefined),
|
||||
* }, null);
|
||||
* };
|
||||
* },
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
//#endregion
|
||||
//#region
|
||||
(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]);
|
||||
const normalized = normalizeClass(childNode.arguments[0], name);
|
||||
if (normalized === null) return;
|
||||
this.replace({
|
||||
type: 'Literal',
|
||||
@@ -219,8 +340,60 @@ export function unwindCssModuleClassName(ast: estree.Node): void {
|
||||
});
|
||||
},
|
||||
});
|
||||
/* This region compiled the `normalizeClass` call into a pseudo-AOT compilation, as in the following code.
|
||||
*
|
||||
* ```ts
|
||||
* const _sfc_main = defineComponent({
|
||||
* setup(_props) {
|
||||
* ...
|
||||
* return (_ctx, _cache) => {
|
||||
* ...
|
||||
* return openBlock(), createElementBlock("div", {
|
||||
* class: normalizeClass("bar"),
|
||||
* }, null);
|
||||
* };
|
||||
* },
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* ↓
|
||||
*
|
||||
* ```ts
|
||||
* const _sfc_main = defineComponent({
|
||||
* setup(_props) {
|
||||
* ...
|
||||
* return (_ctx, _cache) => {
|
||||
* ...
|
||||
* return openBlock(), createElementBlock("div", {
|
||||
* class: "bar",
|
||||
* }, null);
|
||||
* };
|
||||
* },
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
//#endregion
|
||||
}
|
||||
//#region
|
||||
if (node.declarations[0].init.arguments[1].elements.length === 1) {
|
||||
(walk as typeof estreeWalker.walk)(ast, {
|
||||
enter(childNode) {
|
||||
if (childNode.type !== 'Identifier') return;
|
||||
if (childNode.name !== ident) return;
|
||||
this.replace({
|
||||
type: 'Identifier',
|
||||
name: node.declarations[0].id.name,
|
||||
});
|
||||
},
|
||||
});
|
||||
this.remove();
|
||||
/* NOTE: The above logic is valid as long as the following two conditions are met.
|
||||
*
|
||||
* - the uniqueness of `ident` is kept throughout the module
|
||||
* - `_export_sfc` is noop when the second argument is an empty array
|
||||
*
|
||||
* Otherwise, the below logic should be used instead.
|
||||
|
||||
this.replace({
|
||||
type: 'VariableDeclaration',
|
||||
declarations: [{
|
||||
@@ -236,6 +409,7 @@ export function unwindCssModuleClassName(ast: estree.Node): void {
|
||||
}],
|
||||
kind: 'const',
|
||||
});
|
||||
*/
|
||||
} else {
|
||||
this.replace({
|
||||
type: 'VariableDeclaration',
|
||||
@@ -263,6 +437,35 @@ export function unwindCssModuleClassName(ast: estree.Node): void {
|
||||
kind: 'const',
|
||||
});
|
||||
}
|
||||
/* This region removed the `__cssModules` reference from the second argument of `_export_sfc`, as in the following code.
|
||||
*
|
||||
* ```ts
|
||||
* const SomeComponent = _export_sfc(_sfc_main, [["foo", bar], ["__cssModules", cssModules]]);
|
||||
* ```
|
||||
*
|
||||
* ↓
|
||||
*
|
||||
* ```ts
|
||||
* const SomeComponent = _export_sfc(_sfc_main, [["foo", bar]]);
|
||||
* ```
|
||||
*
|
||||
* When the declaration becomes noop, it is removed as follows.
|
||||
*
|
||||
* ```ts
|
||||
* const _sfc_main = defineComponent({
|
||||
* ...
|
||||
* });
|
||||
* const SomeComponent = _export_sfc(_sfc_main, []);
|
||||
* ```
|
||||
*
|
||||
* ↓
|
||||
*
|
||||
* ```ts
|
||||
* const SomeComponent = defineComponent({
|
||||
* ...
|
||||
* });
|
||||
*/
|
||||
//#endregion
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@@ -19,18 +19,16 @@
|
||||
"dependencies": {
|
||||
"@discordapp/twemoji": "14.1.2",
|
||||
"@github/webauthn-json": "2.1.1",
|
||||
"@rollup/plugin-alias": "5.1.0",
|
||||
"@rollup/plugin-json": "6.0.1",
|
||||
"@rollup/plugin-json": "6.1.0",
|
||||
"@rollup/plugin-replace": "5.0.5",
|
||||
"@rollup/plugin-typescript": "11.1.5",
|
||||
"@rollup/pluginutils": "5.1.0",
|
||||
"@syuilo/aiscript": "0.16.0",
|
||||
"@tabler/icons-webfont": "2.37.0",
|
||||
"@vitejs/plugin-vue": "4.5.1",
|
||||
"@vue/compiler-sfc": "3.3.9",
|
||||
"astring": "1.8.6",
|
||||
"autosize": "6.0.1",
|
||||
"@tabler/icons-webfont": "2.44.0",
|
||||
"@vitejs/plugin-vue": "4.5.2",
|
||||
"@vue/compiler-sfc": "3.3.11",
|
||||
"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.0.6",
|
||||
"astring": "1.8.6",
|
||||
"broadcast-channel": "6.0.0",
|
||||
"browser-image-resizer": "github:misskey-dev/browser-image-resizer#v2.2.1-misskey.3",
|
||||
"buraha": "0.0.1",
|
||||
@@ -47,7 +45,7 @@
|
||||
"escape-regexp": "0.0.1",
|
||||
"estree-walker": "3.0.3",
|
||||
"eventemitter3": "5.0.1",
|
||||
"gsap": "3.12.3",
|
||||
"gsap": "3.12.4",
|
||||
"idb-keyval": "6.2.1",
|
||||
"insert-text-at-cursor": "0.3.0",
|
||||
"is-file-animated": "1.0.2",
|
||||
@@ -57,11 +55,10 @@
|
||||
"misskey-js": "workspace:*",
|
||||
"photoswipe": "5.4.3",
|
||||
"punycode": "2.3.1",
|
||||
"querystring": "0.2.1",
|
||||
"rollup": "4.7.0",
|
||||
"rollup": "4.9.0",
|
||||
"sanitize-html": "2.11.0",
|
||||
"shiki": "0.14.6",
|
||||
"sass": "1.69.5",
|
||||
"shiki": "0.14.7",
|
||||
"strict-event-emitter-types": "2.0.0",
|
||||
"textarea-caret": "3.1.0",
|
||||
"three": "0.159.0",
|
||||
@@ -73,30 +70,29 @@
|
||||
"typescript": "5.3.3",
|
||||
"uuid": "9.0.1",
|
||||
"v-code-diff": "1.7.2",
|
||||
"vanilla-tilt": "1.8.1",
|
||||
"vite": "5.0.7",
|
||||
"vite": "5.0.10",
|
||||
"vue": "3.3.11",
|
||||
"vuedraggable": "next"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@storybook/addon-actions": "7.6.4",
|
||||
"@storybook/addon-essentials": "7.6.4",
|
||||
"@storybook/addon-interactions": "7.6.4",
|
||||
"@storybook/addon-links": "7.6.4",
|
||||
"@storybook/addon-storysource": "7.6.4",
|
||||
"@storybook/addons": "7.6.4",
|
||||
"@storybook/blocks": "7.6.4",
|
||||
"@storybook/core-events": "7.6.4",
|
||||
"@storybook/addon-actions": "7.6.5",
|
||||
"@storybook/addon-essentials": "7.6.5",
|
||||
"@storybook/addon-interactions": "7.6.5",
|
||||
"@storybook/addon-links": "7.6.5",
|
||||
"@storybook/addon-storysource": "7.6.5",
|
||||
"@storybook/addons": "7.6.5",
|
||||
"@storybook/blocks": "7.6.5",
|
||||
"@storybook/core-events": "7.6.5",
|
||||
"@storybook/jest": "0.2.3",
|
||||
"@storybook/manager-api": "7.6.4",
|
||||
"@storybook/preview-api": "7.6.4",
|
||||
"@storybook/react": "7.6.4",
|
||||
"@storybook/react-vite": "7.6.4",
|
||||
"@storybook/manager-api": "7.6.5",
|
||||
"@storybook/preview-api": "7.6.5",
|
||||
"@storybook/react": "7.6.5",
|
||||
"@storybook/react-vite": "7.6.5",
|
||||
"@storybook/testing-library": "0.2.2",
|
||||
"@storybook/theming": "7.6.4",
|
||||
"@storybook/types": "7.6.4",
|
||||
"@storybook/vue3": "7.6.4",
|
||||
"@storybook/vue3-vite": "7.6.4",
|
||||
"@storybook/theming": "7.6.5",
|
||||
"@storybook/types": "7.6.5",
|
||||
"@storybook/vue3": "7.6.5",
|
||||
"@storybook/vue3-vite": "7.6.5",
|
||||
"@testing-library/vue": "8.0.1",
|
||||
"@types/escape-regexp": "0.0.3",
|
||||
"@types/estree": "1.0.5",
|
||||
@@ -108,29 +104,29 @@
|
||||
"@types/throttle-debounce": "5.0.2",
|
||||
"@types/tinycolor2": "1.4.6",
|
||||
"@types/uuid": "9.0.7",
|
||||
"@types/websocket": "1.0.10",
|
||||
"@types/ws": "8.5.10",
|
||||
"@typescript-eslint/eslint-plugin": "6.13.2",
|
||||
"@typescript-eslint/parser": "6.13.2",
|
||||
"@typescript-eslint/eslint-plugin": "6.14.0",
|
||||
"@typescript-eslint/parser": "6.14.0",
|
||||
"@vitest/coverage-v8": "0.34.6",
|
||||
"@vue/runtime-core": "3.3.11",
|
||||
"acorn": "8.11.2",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "13.6.1",
|
||||
"eslint": "8.55.0",
|
||||
"eslint-plugin-import": "2.29.0",
|
||||
"eslint": "8.56.0",
|
||||
"eslint-plugin-import": "2.29.1",
|
||||
"eslint-plugin-vue": "9.19.2",
|
||||
"fast-glob": "3.3.2",
|
||||
"happy-dom": "10.0.3",
|
||||
"intersection-observer": "0.12.2",
|
||||
"micromatch": "4.0.5",
|
||||
"msw": "1.3.2",
|
||||
"msw-storybook-addon": "1.10.0",
|
||||
"nodemon": "3.0.2",
|
||||
"prettier": "3.1.0",
|
||||
"prettier": "3.1.1",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"start-server-and-test": "2.0.3",
|
||||
"storybook": "7.6.4",
|
||||
"storybook": "7.6.5",
|
||||
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
|
||||
"summaly": "github:misskey-dev/summaly",
|
||||
"vite-plugin-turbosnap": "1.0.3",
|
||||
|
@@ -8,4 +8,88 @@
|
||||
// (pnpm start時はpugファイルの中で静的リソースとして読み込むようになっており、この問題は起こっていない)
|
||||
import '@tabler/icons-webfont/tabler-icons.scss';
|
||||
|
||||
await main();
|
||||
|
||||
import('@/_boot_.js');
|
||||
|
||||
/**
|
||||
* backend/src/server/web/boot.jsで差し込まれている起動処理のうち、最低限必要なものを模倣するための処理
|
||||
*/
|
||||
async function main() {
|
||||
const forceError = localStorage.getItem('forceError');
|
||||
if (forceError != null) {
|
||||
renderError('FORCED_ERROR', 'This error is forced by having forceError in local storage.');
|
||||
}
|
||||
|
||||
//#region Detect language & fetch translations
|
||||
|
||||
// dev-modeの場合は常に取り直す
|
||||
const supportedLangs = _LANGS_.map(it => it[0]);
|
||||
let lang: string | null | undefined = localStorage.getItem('lang');
|
||||
if (lang == null || !supportedLangs.includes(lang)) {
|
||||
if (supportedLangs.includes(navigator.language)) {
|
||||
lang = navigator.language;
|
||||
} else {
|
||||
lang = supportedLangs.find(x => x.split('-')[0] === navigator.language);
|
||||
|
||||
// Fallback
|
||||
if (lang == null) lang = 'en-US';
|
||||
}
|
||||
}
|
||||
|
||||
// TODO:今のままだと言語ファイル変更後はpnpm devをリスタートする必要があるので、chokidarを使ったり等で対応できるようにする
|
||||
const locale = _LANGS_FULL_.find(it => it[0] === lang);
|
||||
localStorage.setItem('lang', lang);
|
||||
localStorage.setItem('locale', JSON.stringify(locale[1]));
|
||||
localStorage.setItem('localeVersion', _VERSION_);
|
||||
//#endregion
|
||||
|
||||
//#region Theme
|
||||
const theme = localStorage.getItem('theme');
|
||||
if (theme) {
|
||||
for (const [k, v] of Object.entries(JSON.parse(theme))) {
|
||||
document.documentElement.style.setProperty(`--${k}`, v.toString());
|
||||
|
||||
// HTMLの theme-color 適用
|
||||
if (k === 'htmlThemeColor') {
|
||||
for (const tag of document.head.children) {
|
||||
if (tag.tagName === 'META' && tag.getAttribute('name') === 'theme-color') {
|
||||
tag.setAttribute('content', v);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const colorScheme = localStorage.getItem('colorScheme');
|
||||
if (colorScheme) {
|
||||
document.documentElement.style.setProperty('color-scheme', colorScheme);
|
||||
}
|
||||
//#endregion
|
||||
|
||||
const fontSize = localStorage.getItem('fontSize');
|
||||
if (fontSize) {
|
||||
document.documentElement.classList.add('f-' + fontSize);
|
||||
}
|
||||
|
||||
const useSystemFont = localStorage.getItem('useSystemFont');
|
||||
if (useSystemFont) {
|
||||
document.documentElement.classList.add('useSystemFont');
|
||||
}
|
||||
|
||||
const wallpaper = localStorage.getItem('wallpaper');
|
||||
if (wallpaper) {
|
||||
document.documentElement.style.backgroundImage = `url(${wallpaper})`;
|
||||
}
|
||||
|
||||
const customCss = localStorage.getItem('customCss');
|
||||
if (customCss && customCss.length > 0) {
|
||||
const style = document.createElement('style');
|
||||
style.innerHTML = customCss;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
}
|
||||
|
||||
function renderError(code: string, details?: string) {
|
||||
console.log(code, details);
|
||||
}
|
||||
|
@@ -16,7 +16,7 @@ import { unisonReload, reloadChannel } from '@/scripts/unison-reload.js';
|
||||
|
||||
// TODO: 他のタブと永続化されたstateを同期
|
||||
|
||||
type Account = Misskey.entities.MeDetailed;
|
||||
type Account = Misskey.entities.MeDetailed & { token: string };
|
||||
|
||||
const accountData = miLocalStorage.getItem('account');
|
||||
|
||||
@@ -284,7 +284,7 @@ export async function openAccountMenu(opts: {
|
||||
text: i18n.ts.profile,
|
||||
to: `/@${ $i.username }`,
|
||||
avatar: $i,
|
||||
}, null, ...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises, {
|
||||
}, { type: 'divider' }, ...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises, {
|
||||
type: 'parent' as const,
|
||||
icon: 'ti ti-plus',
|
||||
text: i18n.ts.addAccount,
|
||||
|
@@ -8,23 +8,20 @@ import { compareVersions } from 'compare-versions';
|
||||
import widgets from '@/widgets/index.js';
|
||||
import directives from '@/directives/index.js';
|
||||
import components from '@/components/index.js';
|
||||
import { version, ui, lang, updateLocale } from '@/config.js';
|
||||
import { version, ui, lang, updateLocale, locale } from '@/config.js';
|
||||
import { applyTheme } from '@/scripts/theme.js';
|
||||
import { isDeviceDarkmode } from '@/scripts/is-device-darkmode.js';
|
||||
import { i18n, updateI18n } from '@/i18n.js';
|
||||
import { confirm, alert, post, popup, toast } from '@/os.js';
|
||||
import { $i, refreshAccount, login, updateAccount, signout } from '@/account.js';
|
||||
import { defaultStore, ColdDeviceStorage } from '@/store.js';
|
||||
import { fetchInstance, instance } from '@/instance.js';
|
||||
import { deviceKind } from '@/scripts/device-kind.js';
|
||||
import { reloadChannel } from '@/scripts/unison-reload.js';
|
||||
import { reactionPicker } from '@/scripts/reaction-picker.js';
|
||||
import { getUrlWithoutLoginId } from '@/scripts/login-id.js';
|
||||
import { getAccountFromId } from '@/scripts/get-account-from-id.js';
|
||||
import { deckStore } from '@/ui/deck/deck-store.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { fetchCustomEmojis } from '@/custom-emojis.js';
|
||||
import { mainRouter } from '@/router.js';
|
||||
|
||||
export async function common(createVue: () => App<Element>) {
|
||||
console.info(`Misskey v${version}`);
|
||||
@@ -88,7 +85,7 @@ export async function common(createVue: () => App<Element>) {
|
||||
|
||||
//#region Detect language & fetch translations
|
||||
const localeVersion = miLocalStorage.getItem('localeVersion');
|
||||
const localeOutdated = (localeVersion == null || localeVersion !== version);
|
||||
const localeOutdated = (localeVersion == null || localeVersion !== version || locale == null);
|
||||
if (localeOutdated) {
|
||||
const res = await window.fetch(`/assets/locales/${lang}.${version}.json`);
|
||||
if (res.status === 200) {
|
||||
|
@@ -20,6 +20,7 @@ import { mainRouter } from '@/router.js';
|
||||
import { initializeSw } from '@/scripts/initialize-sw.js';
|
||||
import { deckStore } from '@/ui/deck/deck-store.js';
|
||||
import { emojiPicker } from '@/scripts/emoji-picker.js';
|
||||
import { SnowfallEffect } from '@/scripts/snowfall-effect.js';
|
||||
|
||||
export async function mainBoot() {
|
||||
const { isClientUpdated } = await common(() => createApp(
|
||||
@@ -75,6 +76,13 @@ export async function mainBoot() {
|
||||
},
|
||||
};
|
||||
|
||||
if (defaultStore.state.enableSeasonalScreenEffect) {
|
||||
const month = new Date().getMonth() + 1;
|
||||
if (month === 12 || month === 1) {
|
||||
new SnowfallEffect().render();
|
||||
}
|
||||
}
|
||||
|
||||
if ($i) {
|
||||
// only add post shortcuts if logged in
|
||||
hotkeys['p|n'] = post;
|
||||
|
@@ -21,8 +21,9 @@ const props = withDefaults(defineProps<{
|
||||
focus: 1.0,
|
||||
});
|
||||
|
||||
function loadShader(gl, type, source) {
|
||||
function loadShader(gl: WebGLRenderingContext, type: number, source: string) {
|
||||
const shader = gl.createShader(type);
|
||||
if (shader == null) return null;
|
||||
|
||||
try {
|
||||
gl.shaderSource(shader, source);
|
||||
@@ -46,11 +47,13 @@ function loadShader(gl, type, source) {
|
||||
return shader;
|
||||
}
|
||||
|
||||
function initShaderProgram(gl, vsSource, fsSource) {
|
||||
function initShaderProgram(gl: WebGLRenderingContext, vsSource: string, fsSource: string) {
|
||||
const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource);
|
||||
const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource);
|
||||
|
||||
const shaderProgram = gl.createProgram();
|
||||
if (shaderProgram == null || vertexShader == null || fragmentShader == null) return null;
|
||||
|
||||
gl.attachShader(shaderProgram, vertexShader);
|
||||
gl.attachShader(shaderProgram, fragmentShader);
|
||||
gl.linkProgram(shaderProgram);
|
||||
@@ -71,8 +74,10 @@ let handle: ReturnType<typeof window['requestAnimationFrame']> | null = null;
|
||||
|
||||
onMounted(() => {
|
||||
const canvas = canvasEl.value!;
|
||||
canvas.width = canvas.offsetWidth;
|
||||
canvas.height = canvas.offsetHeight;
|
||||
let width = canvas.offsetWidth;
|
||||
let height = canvas.offsetHeight;
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
|
||||
const gl = canvas.getContext('webgl', { premultipliedAlpha: true });
|
||||
if (gl == null) return;
|
||||
@@ -205,6 +210,7 @@ onMounted(() => {
|
||||
gl_FragColor = vec4( color, max(max(color.x, color.y), color.z) );
|
||||
}
|
||||
`);
|
||||
if (shaderProgram == null) return;
|
||||
|
||||
gl.useProgram(shaderProgram);
|
||||
const u_resolution = gl.getUniformLocation(shaderProgram, 'u_resolution');
|
||||
@@ -234,7 +240,23 @@ onMounted(() => {
|
||||
gl!.uniform1f(u_time, 0);
|
||||
gl!.drawArrays(gl!.TRIANGLE_STRIP, 0, 4);
|
||||
} else {
|
||||
function render(timeStamp) {
|
||||
function render(timeStamp: number) {
|
||||
let sizeChanged = false;
|
||||
if (Math.abs(height - canvas.offsetHeight) > 2) {
|
||||
height = canvas.offsetHeight;
|
||||
canvas.height = height;
|
||||
sizeChanged = true;
|
||||
}
|
||||
if (Math.abs(width - canvas.offsetWidth) > 2) {
|
||||
width = canvas.offsetWidth;
|
||||
canvas.width = width;
|
||||
sizeChanged = true;
|
||||
}
|
||||
if (sizeChanged && gl) {
|
||||
gl.uniform2fv(u_resolution, [width, height]);
|
||||
gl.viewport(0, 0, width, height);
|
||||
}
|
||||
|
||||
gl!.uniform1f(u_time, timeStamp);
|
||||
gl!.drawArrays(gl!.TRIANGLE_STRIP, 0, 4);
|
||||
|
||||
|
@@ -54,7 +54,7 @@ watch(() => props.lang, (to) => {
|
||||
return new Promise((resolve) => {
|
||||
fetchLanguage(to).then(() => resolve);
|
||||
});
|
||||
}, { immediate: true, });
|
||||
}, { immediate: true });
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
@@ -9,7 +9,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkLoading v-if="!inline ?? true"/>
|
||||
</template>
|
||||
<code v-if="inline" :class="$style.codeInlineRoot">{{ code }}</code>
|
||||
<XCode v-else-if="show" :code="code" :lang="lang"/>
|
||||
<XCode v-else-if="show && lang" :code="code" :lang="lang"/>
|
||||
<pre v-else-if="show" :class="$style.codeBlockFallbackRoot"><code :class="$style.codeBlockFallbackCode">{{ code }}</code></pre>
|
||||
<button v-else :class="$style.codePlaceholderRoot" @click="show = true">
|
||||
<div :class="$style.codePlaceholderContainer">
|
||||
<div><i class="ti ti-code"></i> {{ i18n.ts.code }}</div>
|
||||
@@ -47,6 +48,21 @@ const XCode = defineAsyncComponent(() => import('@/components/MkCode.core.vue'))
|
||||
border-radius: .3em;
|
||||
}
|
||||
|
||||
.codeBlockFallbackRoot {
|
||||
display: block;
|
||||
overflow-wrap: anywhere;
|
||||
color: #D4D4D4;
|
||||
background: #1E1E1E;
|
||||
padding: 1em;
|
||||
margin: .5em 0;
|
||||
overflow: auto;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.codeBlockFallbackCode {
|
||||
font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
|
||||
}
|
||||
|
||||
.codePlaceholderRoot {
|
||||
display: block;
|
||||
width: 100%;
|
||||
|
@@ -4,31 +4,39 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div :class="[$style.codeEditorRoot, { [$style.disabled]: disabled, [$style.focused]: focused }]">
|
||||
<div :class="$style.codeEditorScroller">
|
||||
<textarea
|
||||
ref="inputEl"
|
||||
v-model="vModel"
|
||||
:class="[$style.textarea]"
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled"
|
||||
:required="required"
|
||||
:readonly="readonly"
|
||||
autocomplete="off"
|
||||
wrap="off"
|
||||
spellcheck="false"
|
||||
@focus="focused = true"
|
||||
@blur="focused = false"
|
||||
@keydown="onKeydown($event)"
|
||||
@input="onInput"
|
||||
></textarea>
|
||||
<XCode :class="$style.codeEditorHighlighter" :codeEditor="true" :code="v" :lang="lang"/>
|
||||
<div>
|
||||
<div :class="$style.label" @click="focus"><slot name="label"></slot></div>
|
||||
<div :class="[$style.codeEditorRoot, { [$style.focused]: focused }]">
|
||||
<div :class="$style.codeEditorScroller">
|
||||
<textarea
|
||||
ref="inputEl"
|
||||
v-model="vModel"
|
||||
:class="[$style.textarea]"
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled"
|
||||
:required="required"
|
||||
:readonly="readonly"
|
||||
autocomplete="off"
|
||||
wrap="off"
|
||||
spellcheck="false"
|
||||
@focus="focused = true"
|
||||
@blur="focused = false"
|
||||
@keydown="onKeydown($event)"
|
||||
@input="onInput"
|
||||
></textarea>
|
||||
<XCode :class="$style.codeEditorHighlighter" :codeEditor="true" :code="v" :lang="lang"/>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.caption"><slot name="caption"></slot></div>
|
||||
<MkButton v-if="manualSave && changed" primary :class="$style.save" @click="updated"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, toRefs, shallowRef, nextTick } from 'vue';
|
||||
import { debounce } from 'throttle-debounce';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import XCode from '@/components/MkCode.core.vue';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
@@ -38,6 +46,8 @@ const props = withDefaults(defineProps<{
|
||||
required?: boolean;
|
||||
readonly?: boolean;
|
||||
disabled?: boolean;
|
||||
debounce?: boolean;
|
||||
manualSave?: boolean;
|
||||
}>(), {
|
||||
lang: 'js',
|
||||
});
|
||||
@@ -56,6 +66,8 @@ const focused = ref(false);
|
||||
const changed = ref(false);
|
||||
const inputEl = shallowRef<HTMLTextAreaElement>();
|
||||
|
||||
const focus = () => inputEl.value?.focus();
|
||||
|
||||
const onInput = (ev) => {
|
||||
v.value = ev.target?.value ?? v.value;
|
||||
changed.value = true;
|
||||
@@ -102,16 +114,48 @@ const updated = () => {
|
||||
emit('update:modelValue', v.value);
|
||||
};
|
||||
|
||||
const debouncedUpdated = debounce(1000, updated);
|
||||
|
||||
watch(modelValue, newValue => {
|
||||
v.value = newValue ?? '';
|
||||
});
|
||||
|
||||
watch(v, () => {
|
||||
updated();
|
||||
watch(v, newValue => {
|
||||
if (!props.manualSave) {
|
||||
if (props.debounce) {
|
||||
debouncedUpdated();
|
||||
} else {
|
||||
updated();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.label {
|
||||
font-size: 0.85em;
|
||||
padding: 0 0 8px 0;
|
||||
user-select: none;
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.caption {
|
||||
font-size: 0.85em;
|
||||
padding: 8px 0 0 0;
|
||||
color: var(--fgTransparentWeak);
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.save {
|
||||
margin: 8px 0 0 0;
|
||||
}
|
||||
|
||||
.codeEditorRoot {
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
@@ -119,6 +163,7 @@ watch(v, () => {
|
||||
overflow-y: hidden;
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
border-radius: 6px;
|
||||
padding: 0;
|
||||
color: var(--fg);
|
||||
border: solid 1px var(--panel);
|
||||
@@ -159,9 +204,10 @@ watch(v, () => {
|
||||
caret-color: rgb(225, 228, 232);
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
outline: 0;
|
||||
min-width: calc(100% - 24px);
|
||||
height: calc(100% - 24px);
|
||||
height: 100%;
|
||||
padding: 12px;
|
||||
line-height: 1.5em;
|
||||
font-size: 1em;
|
||||
|
@@ -39,6 +39,7 @@ import { i18n } from '@/i18n.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { claimAchievement } from '@/scripts/achievements.js';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||
import { MenuItem } from '@/types/menu.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
folder: Misskey.entities.DriveFolder;
|
||||
@@ -250,7 +251,7 @@ function setAsUploadFolder() {
|
||||
}
|
||||
|
||||
function onContextmenu(ev: MouseEvent) {
|
||||
let menu;
|
||||
let menu: MenuItem[];
|
||||
menu = [{
|
||||
text: i18n.ts.openInWindow,
|
||||
icon: 'ti ti-app-window',
|
||||
@@ -260,18 +261,18 @@ function onContextmenu(ev: MouseEvent) {
|
||||
}, {
|
||||
}, 'closed');
|
||||
},
|
||||
}, null, {
|
||||
}, { type: 'divider' }, {
|
||||
text: i18n.ts.rename,
|
||||
icon: 'ti ti-forms',
|
||||
action: rename,
|
||||
}, null, {
|
||||
}, { type: 'divider' }, {
|
||||
text: i18n.ts.delete,
|
||||
icon: 'ti ti-trash',
|
||||
danger: true,
|
||||
action: deleteFolder,
|
||||
}];
|
||||
if (defaultStore.state.devMode) {
|
||||
menu = menu.concat([null, {
|
||||
menu = menu.concat([{ type: 'divider' }, {
|
||||
icon: 'ti ti-id',
|
||||
text: i18n.ts.copyFolderId,
|
||||
action: () => {
|
||||
|
@@ -616,7 +616,7 @@ function getMenu() {
|
||||
type: 'switch',
|
||||
text: i18n.ts.keepOriginalUploading,
|
||||
ref: keepOriginal,
|
||||
}, null, {
|
||||
}, { type: 'divider' }, {
|
||||
text: i18n.ts.addFile,
|
||||
type: 'label',
|
||||
}, {
|
||||
@@ -627,7 +627,7 @@ function getMenu() {
|
||||
text: i18n.ts.fromUrl,
|
||||
icon: 'ti ti-link',
|
||||
action: () => { urlUpload(); },
|
||||
}, null, {
|
||||
}, { type: 'divider' }, {
|
||||
text: folder.value ? folder.value.name : i18n.ts.drive,
|
||||
type: 'label',
|
||||
}, folder.value ? {
|
||||
|
@@ -26,35 +26,35 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</section>
|
||||
<!-- フォルダの中にはカスタム絵文字やフォルダがある -->
|
||||
<section v-else v-panel style="border-radius: 6px; border-bottom: 0.5px solid var(--divider);">
|
||||
<header class="_acrylic" @click="shown = !shown">
|
||||
<header class="_acrylic" @click="shown = !shown">
|
||||
<i class="toggle ti-fw" :class="shown ? 'ti ti-chevron-down' : 'ti ti-chevron-up'"></i> <slot></slot> (<i class="ti ti-folder ti-fw"></i>:{{ customEmojiTree.length }} <i class="ti ti-icons ti-fw"></i>:{{ emojis.length }})
|
||||
</header>
|
||||
<div v-if="shown" style="padding-left: 9px;">
|
||||
<MkEmojiPickerSection
|
||||
v-for="child in customEmojiTree"
|
||||
:key="`custom:${child.category}`"
|
||||
:initialShown="initialShown"
|
||||
:emojis="computed(() => customEmojis.filter(e => e.category === child.category).map(e => `:${e.name}:`))"
|
||||
:hasChildSection="child.children.length !== 0"
|
||||
:customEmojiTree="child.children"
|
||||
@chosen="nestedChosen"
|
||||
>
|
||||
{{ child.category || i18n.ts.other }}
|
||||
</MkEmojiPickerSection>
|
||||
</div>
|
||||
<div v-if="shown" class="body">
|
||||
<button
|
||||
v-for="emoji in emojis"
|
||||
:key="emoji"
|
||||
:data-emoji="emoji"
|
||||
class="_button item"
|
||||
@pointerenter="computeButtonTitle"
|
||||
@click="emit('chosen', emoji, $event)"
|
||||
>
|
||||
<MkCustomEmoji v-if="emoji[0] === ':'" class="emoji" :name="emoji" :normal="true"/>
|
||||
<MkEmoji v-else class="emoji" :emoji="emoji" :normal="true"/>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<div v-if="shown" style="padding-left: 9px;">
|
||||
<MkEmojiPickerSection
|
||||
v-for="child in customEmojiTree"
|
||||
:key="`custom:${child.category}`"
|
||||
:initialShown="initialShown"
|
||||
:emojis="computed(() => customEmojis.filter(e => e.category === child.category).map(e => `:${e.name}:`))"
|
||||
:hasChildSection="child.children.length !== 0"
|
||||
:customEmojiTree="child.children"
|
||||
@chosen="nestedChosen"
|
||||
>
|
||||
{{ child.category || i18n.ts.other }}
|
||||
</MkEmojiPickerSection>
|
||||
</div>
|
||||
<div v-if="shown" class="body">
|
||||
<button
|
||||
v-for="emoji in emojis"
|
||||
:key="emoji"
|
||||
:data-emoji="emoji"
|
||||
class="_button item"
|
||||
@pointerenter="computeButtonTitle"
|
||||
@click="emit('chosen', emoji, $event)"
|
||||
>
|
||||
<MkCustomEmoji v-if="emoji[0] === ':'" class="emoji" :name="emoji" :normal="true"/>
|
||||
<MkEmoji v-else class="emoji" :emoji="emoji" :normal="true"/>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
|
@@ -77,8 +77,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
:key="`custom:${child.category}`"
|
||||
:initialShown="false"
|
||||
:emojis="computed(() => customEmojis.filter(e => child.category === '' ? (e.category === 'null' || !e.category) : e.category === child.category).filter(filterAvailable).map(e => `:${e.name}:`))"
|
||||
:hasChildSection="child.children.length !== 0"
|
||||
:customEmojiTree="child.children"
|
||||
:hasChildSection="child.children.length !== 0"
|
||||
:customEmojiTree="child.children"
|
||||
@chosen="chosen"
|
||||
>
|
||||
{{ child.category || i18n.ts.other }}
|
||||
@@ -103,12 +103,12 @@ import { ref, shallowRef, computed, watch, onMounted } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import XSection from '@/components/MkEmojiPicker.section.vue';
|
||||
import {
|
||||
emojilist,
|
||||
emojiCharByCategory,
|
||||
UnicodeEmojiDef,
|
||||
unicodeEmojiCategories as categories,
|
||||
getEmojiName,
|
||||
CustomEmojiFolderTree
|
||||
emojilist,
|
||||
emojiCharByCategory,
|
||||
UnicodeEmojiDef,
|
||||
unicodeEmojiCategories as categories,
|
||||
getEmojiName,
|
||||
CustomEmojiFolderTree,
|
||||
} from '@/scripts/emojilist.js';
|
||||
import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
||||
import * as os from '@/os.js';
|
||||
@@ -121,10 +121,11 @@ import { $i } from '@/account.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
showPinned?: boolean;
|
||||
asReactionPicker?: boolean;
|
||||
pinnedEmojis?: string[];
|
||||
maxHeight?: number;
|
||||
asDrawer?: boolean;
|
||||
asWindow?: boolean;
|
||||
asReactionPicker?: boolean; // 今は使われてないが将来的に使いそう
|
||||
}>(), {
|
||||
showPinned: true,
|
||||
});
|
||||
@@ -137,24 +138,22 @@ const searchEl = shallowRef<HTMLInputElement>();
|
||||
const emojisEl = shallowRef<HTMLDivElement>();
|
||||
|
||||
const {
|
||||
reactions: pinnedReactions,
|
||||
reactionPickerSize,
|
||||
reactionPickerWidth,
|
||||
reactionPickerHeight,
|
||||
disableShowingAnimatedImages,
|
||||
emojiPickerScale,
|
||||
emojiPickerWidth,
|
||||
emojiPickerHeight,
|
||||
recentlyUsedEmojis,
|
||||
} = defaultStore.reactiveState;
|
||||
|
||||
const pinned = computed(() => props.asReactionPicker ? pinnedReactions.value : []); // TODO: 非リアクションの絵文字ピッカー用のpinned絵文字を設定可能にする?
|
||||
const size = computed(() => props.asReactionPicker ? reactionPickerSize.value : 1);
|
||||
const width = computed(() => props.asReactionPicker ? reactionPickerWidth.value : 3);
|
||||
const height = computed(() => props.asReactionPicker ? reactionPickerHeight.value : 2);
|
||||
const pinned = computed(() => props.pinnedEmojis);
|
||||
const size = computed(() => emojiPickerScale.value);
|
||||
const width = computed(() => emojiPickerWidth.value);
|
||||
const height = computed(() => emojiPickerHeight.value);
|
||||
const q = ref<string>('');
|
||||
const searchResultCustom = ref<Misskey.entities.EmojiSimple[]>([]);
|
||||
const searchResultUnicode = ref<UnicodeEmojiDef[]>([]);
|
||||
const tab = ref<'index' | 'custom' | 'unicode' | 'tags'>('index');
|
||||
|
||||
const customEmojiFolderRoot: CustomEmojiFolderTree = { category: "", children: [] };
|
||||
const customEmojiFolderRoot: CustomEmojiFolderTree = { category: '', children: [] };
|
||||
|
||||
function parseAndMergeCategories(input: string, root: CustomEmojiFolderTree): CustomEmojiFolderTree {
|
||||
const parts = (input && input !== 'null' ? input : '').split(' / ');
|
||||
@@ -180,9 +179,9 @@ function parseAndMergeCategories(input: string, root: CustomEmojiFolderTree): Cu
|
||||
}
|
||||
|
||||
customEmojiCategories.value.forEach(ec => {
|
||||
if (ec !== null) {
|
||||
parseAndMergeCategories(ec, customEmojiFolderRoot);
|
||||
}
|
||||
if (ec !== null) {
|
||||
parseAndMergeCategories(ec, customEmojiFolderRoot);
|
||||
}
|
||||
});
|
||||
|
||||
parseAndMergeCategories('', customEmojiFolderRoot);
|
||||
@@ -332,8 +331,8 @@ watch(q, () => {
|
||||
});
|
||||
|
||||
function filterAvailable(emoji: Misskey.entities.EmojiSimple): boolean {
|
||||
return ((emoji.roleIdsThatCanBeUsedThisEmojiAsReaction === undefined || emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length === 0) || ($i && $i.roles.some(r => emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.includes(r.id)))) &&
|
||||
((emoji.roleIdsThatCanNotBeUsedThisEmojiAsReaction === undefined || emoji.roleIdsThatCanNotBeUsedThisEmojiAsReaction.length === 0) || ($i && !$i.roles.some(r => emoji.roleIdsThatCanNotBeUsedThisEmojiAsReaction.includes(r.id))));
|
||||
return ((emoji.roleIdsThatCanBeUsedThisEmojiAsReaction === undefined || emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length === 0) || (!!$i && $i.roles.some(r => emoji.roleIdsThatCanBeUsedThisEmojiAsReaction?.includes(r.id)))) &&
|
||||
((emoji.roleIdsThatCanNotBeUsedThisEmojiAsReaction === undefined || emoji.roleIdsThatCanNotBeUsedThisEmojiAsReaction.length === 0) || (!!$i && !$i.roles.some(r => emoji.roleIdsThatCanNotBeUsedThisEmojiAsReaction?.includes(r.id))));
|
||||
}
|
||||
|
||||
function focus() {
|
||||
@@ -373,7 +372,7 @@ function chosen(emoji: any, ev?: MouseEvent) {
|
||||
emit('chosen', key);
|
||||
|
||||
// 最近使った絵文字更新
|
||||
if (!pinned.value.includes(key)) {
|
||||
if (!pinned.value?.includes(key)) {
|
||||
let recents = defaultStore.state.recentlyUsedEmojis;
|
||||
recents = recents.filter((emoji: any) => emoji !== key);
|
||||
recents.unshift(key);
|
||||
|
@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
ref="modal"
|
||||
v-slot="{ type, maxHeight }"
|
||||
:zPriority="'middle'"
|
||||
:preferType="asReactionPicker && defaultStore.state.reactionPickerUseDrawerForMobile === false ? 'popup' : 'auto'"
|
||||
:preferType="defaultStore.state.emojiPickerUseDrawerForMobile === false ? 'popup' : 'auto'"
|
||||
:transparentBg="true"
|
||||
:manualShowing="manualShowing"
|
||||
:src="src"
|
||||
@@ -22,6 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
class="_popup _shadow"
|
||||
:class="{ [$style.drawer]: type === 'drawer' }"
|
||||
:showPinned="showPinned"
|
||||
:pinnedEmojis="pinnedEmojis"
|
||||
:asReactionPicker="asReactionPicker"
|
||||
:asDrawer="type === 'drawer'"
|
||||
:max-height="maxHeight"
|
||||
@@ -40,11 +41,13 @@ const props = withDefaults(defineProps<{
|
||||
manualShowing?: boolean | null;
|
||||
src?: HTMLElement;
|
||||
showPinned?: boolean;
|
||||
pinnedEmojis?: string[],
|
||||
asReactionPicker?: boolean;
|
||||
choseAndClose?: boolean;
|
||||
}>(), {
|
||||
manualShowing: null,
|
||||
showPinned: true,
|
||||
pinnedEmojis: undefined,
|
||||
asReactionPicker: false,
|
||||
choseAndClose: true,
|
||||
});
|
||||
|
@@ -104,7 +104,7 @@ async function onClick() {
|
||||
});
|
||||
emit('update:user', {
|
||||
...props.user,
|
||||
withReplies: defaultStore.state.defaultWithReplies
|
||||
withReplies: defaultStore.state.defaultWithReplies,
|
||||
});
|
||||
hasPendingFollowRequestFromYou.value = true;
|
||||
|
||||
|
@@ -26,11 +26,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<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>
|
||||
</MkInput>
|
||||
<MkInput v-else-if="form[item].type === 'string' && !form[item].multiline" v-model="values[item]" type="text">
|
||||
<MkInput v-else-if="form[item].type === 'string' && !form[item].multiline" v-model="values[item]" type="text" :mfmAutocomplete="form[item].treatAsMfm">
|
||||
<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>
|
||||
</MkInput>
|
||||
<MkTextarea v-else-if="form[item].type === 'string' && form[item].multiline" v-model="values[item]">
|
||||
<MkTextarea v-else-if="form[item].type === 'string' && form[item].multiline" v-model="values[item]" :mfmAutocomplete="form[item].treatAsMfm" :mfmPreview="form[item].treatAsMfm">
|
||||
<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>
|
||||
</MkTextarea>
|
||||
|
@@ -43,11 +43,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, nextTick, ref, shallowRef, watch, computed, toRefs } from 'vue';
|
||||
import { onMounted, onUnmounted, nextTick, ref, shallowRef, watch, computed, toRefs } from 'vue';
|
||||
import { debounce } from 'throttle-debounce';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { useInterval } from '@/scripts/use-interval.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { Autocomplete, SuggestionType } from '@/scripts/autocomplete.js';
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string | number | null;
|
||||
@@ -59,6 +60,7 @@ const props = defineProps<{
|
||||
placeholder?: string;
|
||||
autofocus?: boolean;
|
||||
autocomplete?: string;
|
||||
mfmAutocomplete?: boolean | SuggestionType[],
|
||||
autocapitalize?: string;
|
||||
spellcheck?: boolean;
|
||||
step?: any;
|
||||
@@ -94,6 +96,7 @@ const height =
|
||||
props.small ? 33 :
|
||||
props.large ? 39 :
|
||||
36;
|
||||
let autocomplete: Autocomplete;
|
||||
|
||||
const focus = () => inputEl.value.focus();
|
||||
const onInput = (ev: KeyboardEvent) => {
|
||||
@@ -167,6 +170,16 @@ onMounted(() => {
|
||||
focus();
|
||||
}
|
||||
});
|
||||
|
||||
if (props.mfmAutocomplete) {
|
||||
autocomplete = new Autocomplete(inputEl.value, v, props.mfmAutocomplete === true ? null : props.mfmAutocomplete);
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (autocomplete) {
|
||||
autocomplete.detach();
|
||||
}
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
|
@@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
@contextmenu.self="e => e.preventDefault()"
|
||||
>
|
||||
<template v-for="(item, i) in items2">
|
||||
<div v-if="item === null" role="separator" :class="$style.divider"></div>
|
||||
<div v-if="item.type === 'divider'" role="separator" :class="$style.divider"></div>
|
||||
<span v-else-if="item.type === 'label'" role="menuitem" :class="[$style.label, $style.item]">
|
||||
<span>{{ item.text }}</span>
|
||||
</span>
|
||||
|
@@ -463,7 +463,7 @@ function showRenoteMenu(viaKeyboard = false): void {
|
||||
pleaseLogin();
|
||||
os.popupMenu([
|
||||
getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote),
|
||||
null,
|
||||
{ type: 'divider' },
|
||||
getUnrenote(),
|
||||
], renoteTime.value, {
|
||||
viaKeyboard: viaKeyboard,
|
||||
@@ -471,7 +471,7 @@ function showRenoteMenu(viaKeyboard = false): void {
|
||||
} else {
|
||||
os.popupMenu([
|
||||
getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote),
|
||||
null,
|
||||
{ type: 'divider' },
|
||||
getAbuseNoteMenu(note.value, i18n.ts.reportAbuseRenote),
|
||||
$i.isModerator || $i.isAdmin ? getUnrenote() : undefined,
|
||||
], renoteTime.value, {
|
||||
|
@@ -145,7 +145,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<button class="_button" :class="[$style.tab, { [$style.tabActive]: tab === 'reactions' }]" @click="tab = 'reactions'"><i class="ti ti-icons"></i> {{ i18n.ts.reactions }}</button>
|
||||
</div>
|
||||
<div>
|
||||
<div v-if="tab === 'replies'" :class="$style.tab_replies">
|
||||
<div v-if="tab === 'replies'">
|
||||
<div v-if="!repliesLoaded" style="padding: 16px">
|
||||
<MkButton style="margin: 0 auto;" primary rounded @click="loadReplies">{{ i18n.ts.loadReplies }}</MkButton>
|
||||
</div>
|
||||
|
@@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
import { ref, shallowRef } from 'vue';
|
||||
import MkModal from './MkModal.vue';
|
||||
import MkMenu from './MkMenu.vue';
|
||||
import { MenuItem } from '@/types/menu';
|
||||
import { MenuItem } from '@/types/menu.js';
|
||||
|
||||
defineProps<{
|
||||
items: MenuItem[];
|
||||
|
@@ -860,7 +860,7 @@ async function insertEmoji(ev: MouseEvent) {
|
||||
},
|
||||
() => {
|
||||
textAreaReadOnly.value = false;
|
||||
focus();
|
||||
nextTick(() => focus());
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@@ -139,12 +139,14 @@ if (!mock) {
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
display: inline-block;
|
||||
display: inline-flex;
|
||||
height: 42px;
|
||||
margin: 2px;
|
||||
padding: 0 6px;
|
||||
font-size: 1.5em;
|
||||
border-radius: 6px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&.canToggle {
|
||||
background: var(--buttonBg);
|
||||
@@ -183,7 +185,7 @@ if (!mock) {
|
||||
&.reacted, &.reacted:hover {
|
||||
background: var(--accentedBg);
|
||||
color: var(--accent);
|
||||
box-shadow: 0 0 0px 1px var(--accent) inset;
|
||||
box-shadow: 0 0 0 1px var(--accent) inset;
|
||||
|
||||
> .count {
|
||||
color: var(--accent);
|
||||
|
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div :class="[$style.root, { [$style.disabled]: disabled, [$style.checked]: checked }]">
|
||||
<div :class="[$style.root, { [$style.disabled]: disabled }]">
|
||||
<input
|
||||
ref="input"
|
||||
type="checkbox"
|
||||
@@ -64,9 +64,6 @@ const toggle = () => {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
//&.checked {
|
||||
//}
|
||||
}
|
||||
|
||||
.input {
|
||||
|
@@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
:readonly="readonly"
|
||||
:placeholder="placeholder"
|
||||
:pattern="pattern"
|
||||
:autocomplete="autocomplete"
|
||||
:autocomplete="props.autocomplete"
|
||||
:spellcheck="spellcheck"
|
||||
@focus="focused = true"
|
||||
@blur="focused = false"
|
||||
@@ -26,16 +26,21 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
></textarea>
|
||||
</div>
|
||||
<div :class="$style.caption"><slot name="caption"></slot></div>
|
||||
<button v-if="mfmPreview" style="font-size: 0.85em;" class="_textButton" type="button" @click="preview = !preview">{{ i18n.ts.preview }}</button>
|
||||
<div v-if="mfmPreview" v-show="preview" v-panel :class="$style.mfmPreview">
|
||||
<Mfm :text="v"/>
|
||||
</div>
|
||||
|
||||
<MkButton v-if="manualSave && changed" primary :class="$style.save" @click="updated"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, nextTick, ref, watch, computed, toRefs, shallowRef } from 'vue';
|
||||
import { onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs, shallowRef } from 'vue';
|
||||
import { debounce } from 'throttle-debounce';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { Autocomplete, SuggestionType } from '@/scripts/autocomplete.js';
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string | null;
|
||||
@@ -46,6 +51,8 @@ const props = defineProps<{
|
||||
placeholder?: string;
|
||||
autofocus?: boolean;
|
||||
autocomplete?: string;
|
||||
mfmAutocomplete?: boolean | SuggestionType[],
|
||||
mfmPreview?: boolean;
|
||||
spellcheck?: boolean;
|
||||
debounce?: boolean;
|
||||
manualSave?: boolean;
|
||||
@@ -68,6 +75,8 @@ const changed = ref(false);
|
||||
const invalid = ref(false);
|
||||
const filled = computed(() => v.value !== '' && v.value != null);
|
||||
const inputEl = shallowRef<HTMLTextAreaElement>();
|
||||
const preview = ref(false);
|
||||
let autocomplete: Autocomplete;
|
||||
|
||||
const focus = () => inputEl.value.focus();
|
||||
const onInput = (ev) => {
|
||||
@@ -82,6 +91,16 @@ const onKeydown = (ev: KeyboardEvent) => {
|
||||
if (ev.code === 'Enter') {
|
||||
emit('enter');
|
||||
}
|
||||
|
||||
if (props.code && ev.key === 'Tab') {
|
||||
const pos = inputEl.value?.selectionStart ?? 0;
|
||||
const posEnd = inputEl.value?.selectionEnd ?? v.value.length;
|
||||
v.value = v.value.slice(0, pos) + '\t' + v.value.slice(posEnd);
|
||||
nextTick(() => {
|
||||
inputEl.value?.setSelectionRange(pos + 1, pos + 1);
|
||||
});
|
||||
ev.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
const updated = () => {
|
||||
@@ -113,6 +132,16 @@ onMounted(() => {
|
||||
focus();
|
||||
}
|
||||
});
|
||||
|
||||
if (props.mfmAutocomplete) {
|
||||
autocomplete = new Autocomplete(inputEl.value, v, props.mfmAutocomplete === true ? null : props.mfmAutocomplete);
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (autocomplete) {
|
||||
autocomplete.detach();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -194,4 +223,12 @@ onMounted(() => {
|
||||
.save {
|
||||
margin: 8px 0 0 0;
|
||||
}
|
||||
|
||||
.mfmPreview {
|
||||
padding: 12px;
|
||||
border-radius: var(--radius);
|
||||
box-sizing: border-box;
|
||||
min-height: 130px;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
|
@@ -103,7 +103,7 @@ function showMenu(ev) {
|
||||
action: () => {
|
||||
os.pageWindow('/about-misskey');
|
||||
},
|
||||
}, null, (instance.impressumUrl) ? {
|
||||
}, { type: 'divider' }, (instance.impressumUrl) ? {
|
||||
text: i18n.ts.impressum,
|
||||
icon: 'ti ti-file-invoice',
|
||||
action: () => {
|
||||
@@ -121,7 +121,7 @@ function showMenu(ev) {
|
||||
action: () => {
|
||||
window.open(instance.privacyPolicyUrl, '_blank', 'noopener');
|
||||
},
|
||||
} : undefined, (!instance.impressumUrl && !instance.tosUrl && !instance.privacyPolicyUrl) ? undefined : null, {
|
||||
} : undefined, (!instance.impressumUrl && !instance.tosUrl && !instance.privacyPolicyUrl) ? undefined : { type: 'divider' }, {
|
||||
text: i18n.ts.help,
|
||||
icon: 'ti ti-help-circle',
|
||||
action: () => {
|
||||
|
@@ -56,7 +56,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
import { onBeforeUnmount, onMounted, provide, shallowRef, ref } from 'vue';
|
||||
import contains from '@/scripts/contains.js';
|
||||
import * as os from '@/os.js';
|
||||
import { MenuItem } from '@/types/menu';
|
||||
import { MenuItem } from '@/types/menu.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
|
||||
|
@@ -6,6 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<template>
|
||||
<div :class="[$style.root, { [$style.rootFirst]: first }]">
|
||||
<div :class="[$style.label, { [$style.labelFirst]: first }]"><slot name="label"></slot></div>
|
||||
<div :class="[$style.description]"><slot name="description"></slot></div>
|
||||
<div :class="$style.main">
|
||||
<slot></slot>
|
||||
</div>
|
||||
@@ -31,7 +32,7 @@ defineProps<{
|
||||
.label {
|
||||
font-weight: bold;
|
||||
padding: 1.5em 0 0 0;
|
||||
margin: 0 0 16px 0;
|
||||
margin: 0 0 8px 0;
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
@@ -45,4 +46,10 @@ defineProps<{
|
||||
.main {
|
||||
margin: 1.5em 0 0 0;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 0.85em;
|
||||
color: var(--fgTransparentWeak);
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
</style>
|
||||
|
@@ -57,7 +57,7 @@ function onContextmenu(ev) {
|
||||
action: () => {
|
||||
router.push(props.to, 'forcePage');
|
||||
},
|
||||
}, null, {
|
||||
}, { type: 'divider' }, {
|
||||
icon: 'ti ti-external-link',
|
||||
text: i18n.ts.openInNewTab,
|
||||
action: () => {
|
||||
|
@@ -23,16 +23,19 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<img
|
||||
v-if="showDecoration && (decoration || user.avatarDecorations.length > 0)"
|
||||
:class="[$style.decoration]"
|
||||
:src="decoration?.url ?? user.avatarDecorations[0].url"
|
||||
:style="{
|
||||
rotate: getDecorationAngle(),
|
||||
scale: getDecorationScale(),
|
||||
}"
|
||||
alt=""
|
||||
>
|
||||
<template v-if="showDecoration">
|
||||
<img
|
||||
v-for="decoration in decorations ?? user.avatarDecorations"
|
||||
:class="[$style.decoration]"
|
||||
:src="decoration.url"
|
||||
:style="{
|
||||
rotate: getDecorationAngle(decoration),
|
||||
scale: getDecorationScale(decoration),
|
||||
translate: getDecorationOffset(decoration),
|
||||
}"
|
||||
alt=""
|
||||
>
|
||||
</template>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
@@ -57,19 +60,14 @@ const props = withDefaults(defineProps<{
|
||||
link?: boolean;
|
||||
preview?: boolean;
|
||||
indicator?: boolean;
|
||||
decoration?: {
|
||||
url: string;
|
||||
angle?: number;
|
||||
flipH?: boolean;
|
||||
flipV?: boolean;
|
||||
};
|
||||
decorations?: Omit<Misskey.entities.UserDetailed['avatarDecorations'][number], 'id'>[];
|
||||
forceShowDecoration?: boolean;
|
||||
}>(), {
|
||||
target: null,
|
||||
link: false,
|
||||
preview: false,
|
||||
indicator: false,
|
||||
decoration: undefined,
|
||||
decorations: undefined,
|
||||
forceShowDecoration: false,
|
||||
});
|
||||
|
||||
@@ -92,30 +90,22 @@ function onClick(ev: MouseEvent): void {
|
||||
emit('click', ev);
|
||||
}
|
||||
|
||||
function getDecorationAngle() {
|
||||
let angle;
|
||||
if (props.decoration) {
|
||||
angle = props.decoration.angle ?? 0;
|
||||
} else if (props.user.avatarDecorations.length > 0) {
|
||||
angle = props.user.avatarDecorations[0].angle ?? 0;
|
||||
} else {
|
||||
angle = 0;
|
||||
}
|
||||
function getDecorationAngle(decoration: Omit<Misskey.entities.UserDetailed['avatarDecorations'][number], 'id'>) {
|
||||
const angle = decoration.angle ?? 0;
|
||||
return angle === 0 ? undefined : `${angle * 360}deg`;
|
||||
}
|
||||
|
||||
function getDecorationScale() {
|
||||
let scaleX;
|
||||
if (props.decoration) {
|
||||
scaleX = props.decoration.flipH ? -1 : 1;
|
||||
} else if (props.user.avatarDecorations.length > 0) {
|
||||
scaleX = props.user.avatarDecorations[0].flipH ? -1 : 1;
|
||||
} else {
|
||||
scaleX = 1;
|
||||
}
|
||||
function getDecorationScale(decoration: Omit<Misskey.entities.UserDetailed['avatarDecorations'][number], 'id'>) {
|
||||
const scaleX = decoration.flipH ? -1 : 1;
|
||||
return scaleX === 1 ? undefined : `${scaleX} 1`;
|
||||
}
|
||||
|
||||
function getDecorationOffset(decoration: Omit<Misskey.entities.UserDetailed['avatarDecorations'][number], 'id'>) {
|
||||
const offsetX = decoration.offsetX ?? 0;
|
||||
const offsetY = decoration.offsetY ?? 0;
|
||||
return offsetX === 0 && offsetY === 0 ? undefined : `${offsetX * 100}% ${offsetY * 100}%`;
|
||||
}
|
||||
|
||||
const color = ref<string | undefined>();
|
||||
|
||||
watch(() => props.user.avatarBlurhash, () => {
|
||||
|
@@ -25,6 +25,7 @@ import { defaultStore } from '@/store.js';
|
||||
import { customEmojisMap } from '@/custom-emojis.js';
|
||||
import * as os from '@/os.js';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||
import * as sound from '@/scripts/sound.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -90,6 +91,7 @@ function onClick(ev: MouseEvent) {
|
||||
icon: 'ti ti-plus',
|
||||
action: () => {
|
||||
react(`:${props.name}:`);
|
||||
sound.play('reaction');
|
||||
},
|
||||
}] : [])], ev.currentTarget ?? ev.target);
|
||||
}
|
||||
|
@@ -16,6 +16,7 @@ import { defaultStore } from '@/store.js';
|
||||
import { getEmojiName } from '@/scripts/emojilist.js';
|
||||
import * as os from '@/os.js';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||
import * as sound from '@/scripts/sound.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -56,6 +57,7 @@ function onClick(ev: MouseEvent) {
|
||||
icon: 'ti ti-plus',
|
||||
action: () => {
|
||||
react(props.emoji);
|
||||
sound.play('reaction');
|
||||
},
|
||||
}] : [])], ev.currentTarget ?? ev.target);
|
||||
}
|
||||
|
53
packages/frontend/src/components/global/MkLazy.vue
Normal file
53
packages/frontend/src/components/global/MkLazy.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div ref="rootEl" :class="$style.root">
|
||||
<div v-if="!showing" :class="$style.placeholder"></div>
|
||||
<slot v-else></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { nextTick, onMounted, onActivated, onBeforeUnmount, ref, shallowRef } from 'vue';
|
||||
|
||||
const rootEl = shallowRef<HTMLDivElement>();
|
||||
const showing = ref(false);
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries.some((entry) => entry.isIntersecting)) {
|
||||
showing.value = true;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
observer.observe(rootEl.value!);
|
||||
});
|
||||
});
|
||||
|
||||
onActivated(() => {
|
||||
nextTick(() => {
|
||||
observer.observe(rootEl.value!);
|
||||
});
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
observer.disconnect();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
display: block;
|
||||
min-height: 150px;
|
||||
}
|
||||
</style>
|
@@ -37,7 +37,7 @@ type MfmProps = {
|
||||
isNote?: boolean;
|
||||
emojiUrls?: string[];
|
||||
rootScale?: number;
|
||||
nyaize: boolean | 'respect';
|
||||
nyaize?: boolean | 'respect';
|
||||
parsedNodes?: mfm.MfmNode[] | null;
|
||||
enableEmojiMenu?: boolean;
|
||||
enableEmojiMenuReaction?: boolean;
|
||||
@@ -107,22 +107,26 @@ export default function(props: MfmProps) {
|
||||
switch (token.props.name) {
|
||||
case 'tada': {
|
||||
const speed = validTime(token.props.args.speed) ?? '1s';
|
||||
style = 'font-size: 150%;' + (useAnim ? `animation: tada ${speed} linear infinite both;` : '');
|
||||
const delay = validTime(token.props.args.delay) ?? '0s';
|
||||
style = 'font-size: 150%;' + (useAnim ? `animation: tada ${speed} linear infinite both; animation-delay: ${delay};` : '');
|
||||
break;
|
||||
}
|
||||
case 'jelly': {
|
||||
const speed = validTime(token.props.args.speed) ?? '1s';
|
||||
style = (useAnim ? `animation: mfm-rubberBand ${speed} linear infinite both;` : '');
|
||||
const delay = validTime(token.props.args.delay) ?? '0s';
|
||||
style = (useAnim ? `animation: mfm-rubberBand ${speed} linear infinite both; animation-delay: ${delay};` : '');
|
||||
break;
|
||||
}
|
||||
case 'twitch': {
|
||||
const speed = validTime(token.props.args.speed) ?? '0.5s';
|
||||
style = useAnim ? `animation: mfm-twitch ${speed} ease infinite;` : '';
|
||||
const delay = validTime(token.props.args.delay) ?? '0s';
|
||||
style = useAnim ? `animation: mfm-twitch ${speed} ease infinite; animation-delay: ${delay};` : '';
|
||||
break;
|
||||
}
|
||||
case 'shake': {
|
||||
const speed = validTime(token.props.args.speed) ?? '0.5s';
|
||||
style = useAnim ? `animation: mfm-shake ${speed} ease infinite;` : '';
|
||||
const delay = validTime(token.props.args.delay) ?? '0s';
|
||||
style = useAnim ? `animation: mfm-shake ${speed} ease infinite; animation-delay: ${delay};` : '';
|
||||
break;
|
||||
}
|
||||
case 'spin': {
|
||||
@@ -135,17 +139,20 @@ export default function(props: MfmProps) {
|
||||
token.props.args.y ? 'mfm-spinY' :
|
||||
'mfm-spin';
|
||||
const speed = validTime(token.props.args.speed) ?? '1.5s';
|
||||
style = useAnim ? `animation: ${anime} ${speed} linear infinite; animation-direction: ${direction};` : '';
|
||||
const delay = validTime(token.props.args.delay) ?? '0s';
|
||||
style = useAnim ? `animation: ${anime} ${speed} linear infinite; animation-direction: ${direction}; animation-delay: ${delay};` : '';
|
||||
break;
|
||||
}
|
||||
case 'jump': {
|
||||
const speed = validTime(token.props.args.speed) ?? '0.75s';
|
||||
style = useAnim ? `animation: mfm-jump ${speed} linear infinite;` : '';
|
||||
const delay = validTime(token.props.args.delay) ?? '0s';
|
||||
style = useAnim ? `animation: mfm-jump ${speed} linear infinite; animation-delay: ${delay};` : '';
|
||||
break;
|
||||
}
|
||||
case 'bounce': {
|
||||
const speed = validTime(token.props.args.speed) ?? '0.75s';
|
||||
style = useAnim ? `animation: mfm-bounce ${speed} linear infinite; transform-origin: center bottom;` : '';
|
||||
const delay = validTime(token.props.args.delay) ?? '0s';
|
||||
style = useAnim ? `animation: mfm-bounce ${speed} linear infinite; transform-origin: center bottom; animation-delay: ${delay};` : '';
|
||||
break;
|
||||
}
|
||||
case 'flip': {
|
||||
@@ -195,7 +202,8 @@ export default function(props: MfmProps) {
|
||||
}, genEl(token.children, scale));
|
||||
}
|
||||
const speed = validTime(token.props.args.speed) ?? '1s';
|
||||
style = `animation: mfm-rainbow ${speed} linear infinite;`;
|
||||
const delay = validTime(token.props.args.delay) ?? '0s';
|
||||
style = `animation: mfm-rainbow ${speed} linear infinite; animation-delay: ${delay};`;
|
||||
break;
|
||||
}
|
||||
case 'sparkle': {
|
||||
|
@@ -25,6 +25,7 @@ import MkPageHeader from './global/MkPageHeader.vue';
|
||||
import MkSpacer from './global/MkSpacer.vue';
|
||||
import MkFooterSpacer from './global/MkFooterSpacer.vue';
|
||||
import MkStickyContainer from './global/MkStickyContainer.vue';
|
||||
import MkLazy from './global/MkLazy.vue';
|
||||
|
||||
export default function(app: App) {
|
||||
for (const [key, value] of Object.entries(components)) {
|
||||
@@ -53,6 +54,7 @@ export const components = {
|
||||
MkSpacer: MkSpacer,
|
||||
MkFooterSpacer: MkFooterSpacer,
|
||||
MkStickyContainer: MkStickyContainer,
|
||||
MkLazy: MkLazy,
|
||||
};
|
||||
|
||||
declare module '@vue/runtime-core' {
|
||||
@@ -77,5 +79,6 @@ declare module '@vue/runtime-core' {
|
||||
MkSpacer: typeof MkSpacer;
|
||||
MkFooterSpacer: typeof MkFooterSpacer;
|
||||
MkStickyContainer: typeof MkStickyContainer;
|
||||
MkLazy: typeof MkLazy;
|
||||
}
|
||||
}
|
||||
|
@@ -84,6 +84,7 @@ export const ROLE_POLICIES = [
|
||||
'userListLimit',
|
||||
'userEachUserListsLimit',
|
||||
'rateLimitFactor',
|
||||
'avatarDecorationLimit',
|
||||
] as const;
|
||||
|
||||
// なんか動かない
|
||||
|
@@ -12,17 +12,20 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>misskey</title>
|
||||
<title>[DEV] Loading...</title>
|
||||
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="default-src 'self';
|
||||
script-src 'self';
|
||||
style-src 'self' 'unsafe-inline';
|
||||
img-src 'self' data: www.google.com xn--931a.moe localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000;
|
||||
media-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000;"
|
||||
worker-src 'self';
|
||||
script-src 'self' 'unsafe-eval';
|
||||
style-src 'self' 'unsafe-inline';
|
||||
img-src 'self' data: www.google.com xn--931a.moe localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000;
|
||||
media-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000;
|
||||
connect-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000;"
|
||||
/>
|
||||
<meta property="og:site_name" content="[DEV BUILD] Misskey" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
@@ -546,7 +546,7 @@ export async function openEmojiPicker(src?: HTMLElement, opts, initialTextarea:
|
||||
});
|
||||
}
|
||||
|
||||
export function popupMenu(items: MenuItem[] | Ref<MenuItem[]>, src?: HTMLElement, options?: {
|
||||
export function popupMenu(items: MenuItem[] | Ref<MenuItem[]>, src?: HTMLElement | EventTarget | null, options?: {
|
||||
align?: string;
|
||||
width?: number;
|
||||
viaKeyboard?: boolean;
|
||||
|
@@ -135,10 +135,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<div v-else-if="tab === 'roles'" class="_gaps">
|
||||
<MkButton primary rounded @click="assignRole"><i class="ti ti-plus"></i> {{ i18n.ts.assign }}</MkButton>
|
||||
|
||||
<div v-for="role in info.roles" :key="role.id" :class="$style.roleItem">
|
||||
<div v-for="role in info.roles" :key="role.id">
|
||||
<div :class="$style.roleItemMain">
|
||||
<MkRolePreview :class="$style.role" :role="role" :forModeration="true"/>
|
||||
<button class="_button" :class="$style.roleToggle" @click="toggleRoleItem(role)"><i class="ti ti-chevron-down"></i></button>
|
||||
<button class="_button" @click="toggleRoleItem(role)"><i class="ti ti-chevron-down"></i></button>
|
||||
<button v-if="role.target === 'manual'" class="_button" :class="$style.roleUnassign" @click="unassignRole(role, $event)"><i class="ti ti-x"></i></button>
|
||||
<button v-else class="_button" :class="$style.roleUnassign" disabled><i class="ti ti-ban"></i></button>
|
||||
</div>
|
||||
@@ -330,41 +330,41 @@ async function toggleSuspend(v) {
|
||||
}
|
||||
|
||||
async function unsetUserAvatar() {
|
||||
const confirm = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.ts.unsetUserAvatarConfirm,
|
||||
});
|
||||
if (confirm.canceled) return;
|
||||
const process = async () => {
|
||||
await os.api('admin/unset-user-avatar', { userId: user.value.id });
|
||||
os.success();
|
||||
};
|
||||
await process().catch(err => {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: err.toString(),
|
||||
});
|
||||
});
|
||||
refreshUser();
|
||||
const confirm = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.ts.unsetUserAvatarConfirm,
|
||||
});
|
||||
if (confirm.canceled) return;
|
||||
const process = async () => {
|
||||
await os.api('admin/unset-user-avatar', { userId: user.value.id });
|
||||
os.success();
|
||||
};
|
||||
await process().catch(err => {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: err.toString(),
|
||||
});
|
||||
});
|
||||
refreshUser();
|
||||
}
|
||||
|
||||
async function unsetUserBanner() {
|
||||
const confirm = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.ts.unsetUserBannerConfirm,
|
||||
});
|
||||
if (confirm.canceled) return;
|
||||
const process = async () => {
|
||||
await os.api('admin/unset-user-banner', { userId: user.value.id });
|
||||
os.success();
|
||||
};
|
||||
await process().catch(err => {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: err.toString(),
|
||||
});
|
||||
});
|
||||
refreshUser();
|
||||
const confirm = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.ts.unsetUserBannerConfirm,
|
||||
});
|
||||
if (confirm.canceled) return;
|
||||
const process = async () => {
|
||||
await os.api('admin/unset-user-banner', { userId: user.value.id });
|
||||
os.success();
|
||||
};
|
||||
await process().catch(err => {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: err.toString(),
|
||||
});
|
||||
});
|
||||
refreshUser();
|
||||
}
|
||||
|
||||
async function deleteAllFiles() {
|
||||
@@ -639,9 +639,6 @@ definePageMetadata(computed(() => ({
|
||||
}
|
||||
}
|
||||
|
||||
.roleItem {
|
||||
}
|
||||
|
||||
.roleItemMain {
|
||||
display: flex;
|
||||
}
|
||||
|
@@ -40,7 +40,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkInput ref="announceTitleEl" v-model="announcement.title" :large="false">
|
||||
<template #label>{{ i18n.ts.title }} <button v-tooltip="i18n.ts.emoji" :class="['_button']" @click="insertEmoji"><i class="ti ti-mood-happy"></i></button></template>
|
||||
</MkInput>
|
||||
<MkTextarea v-model="announcement.text">
|
||||
<MkTextarea v-model="announcement.text" mfmAutocomplete :mfmPreview="true">
|
||||
<template #label>{{ i18n.ts.text }}</template>
|
||||
</MkTextarea>
|
||||
<MkInput v-model="announcement.imageUrl" type="url">
|
||||
@@ -100,7 +100,6 @@ import * as misskey from 'misskey-js';
|
||||
import XHeader from './_header_.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import MkRadios from '@/components/MkRadios.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
@@ -114,6 +113,7 @@ const announceTitleEl = shallowRef<HTMLInputElement | null>(null);
|
||||
const user = ref<misskey.entities.UserLite | null>(null);
|
||||
const offset = ref(0);
|
||||
const hasMore = ref(false);
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
|
||||
const announcements = ref<any[]>([]);
|
||||
|
||||
|
@@ -48,7 +48,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkTime :time="log.createdAt"/>
|
||||
</template>
|
||||
|
||||
<div :class="$style.root">
|
||||
<div>
|
||||
<div style="display: flex; gap: var(--margin); flex-wrap: wrap;">
|
||||
<div style="flex: 1;">{{ i18n.ts.moderator }}: <MkA :to="`/admin/user/${log.userId}`" class="_link">@{{ log.user?.username }}</MkA></div>
|
||||
<div style="flex: 1;">{{ i18n.ts.dateAndTime }}: <MkTime :time="log.createdAt" mode="detail"/></div>
|
||||
@@ -134,9 +134,6 @@ const props = defineProps<{
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
|
@@ -591,6 +591,26 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</MkRange>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.avatarDecorationLimit, 'avatarDecorationLimit'])">
|
||||
<template #label>{{ i18n.ts._role._options.avatarDecorationLimit }}</template>
|
||||
<template #suffix>
|
||||
<span v-if="role.policies.avatarDecorationLimit.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||
<span v-else>{{ role.policies.avatarDecorationLimit.value }}</span>
|
||||
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.avatarDecorationLimit)"></i></span>
|
||||
</template>
|
||||
<div class="_gaps">
|
||||
<MkSwitch v-model="role.policies.avatarDecorationLimit.useDefault" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
|
||||
</MkSwitch>
|
||||
<MkInput v-model="role.policies.avatarDecorationLimit.value" type="number" :min="0">
|
||||
<template #label>{{ i18n.ts._role._options.avatarDecorationLimit }}</template>
|
||||
</MkInput>
|
||||
<MkRange v-model="role.policies.avatarDecorationLimit.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
|
||||
<template #label>{{ i18n.ts._role.priority }}</template>
|
||||
</MkRange>
|
||||
</div>
|
||||
</MkFolder>
|
||||
</div>
|
||||
</FormSlot>
|
||||
</div>
|
||||
@@ -609,7 +629,7 @@ import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import MkRange from '@/components/MkRange.vue';
|
||||
import FormSlot from '@/components/form/slot.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { ROLE_POLICIES } from '@/const';
|
||||
import { ROLE_POLICIES } from '@/const.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import { deepClone } from '@/scripts/clone.js';
|
||||
|
||||
|
@@ -216,6 +216,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</MkSwitch>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.avatarDecorationLimit, 'avatarDecorationLimit'])">
|
||||
<template #label>{{ i18n.ts._role._options.avatarDecorationLimit }}</template>
|
||||
<template #suffix>{{ policies.avatarDecorationLimit }}</template>
|
||||
<MkInput v-model="policies.avatarDecorationLimit" type="number" :min="0">
|
||||
</MkInput>
|
||||
</MkFolder>
|
||||
|
||||
<MkButton primary rounded @click="updateBaseRole">{{ i18n.ts.save }}</MkButton>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
@@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<template #label>{{ i18n.ts.name }}</template>
|
||||
</MkInput>
|
||||
|
||||
<MkTextarea v-model="description">
|
||||
<MkTextarea v-model="description" mfmAutocomplete :mfmPreview="true">
|
||||
<template #label>{{ i18n.ts.description }}</template>
|
||||
</MkTextarea>
|
||||
|
||||
@@ -70,7 +70,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, watch, defineAsyncComponent } from 'vue';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkColorInput from '@/components/MkColorInput.vue';
|
||||
@@ -81,6 +80,7 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
|
||||
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
|
||||
|
||||
|
@@ -101,6 +101,7 @@ const headerActions = computed(() => clip.value && isOwned.value ? [{
|
||||
type: 'string',
|
||||
required: false,
|
||||
multiline: true,
|
||||
treatAsMfm: true,
|
||||
label: i18n.ts.description,
|
||||
default: clip.value.description,
|
||||
},
|
||||
|
@@ -15,9 +15,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<template #label>{{ i18n.ts._play.summary }}</template>
|
||||
</MkTextarea>
|
||||
<MkButton primary @click="selectPreset">{{ i18n.ts.selectFromPresets }}<i class="ti ti-chevron-down"></i></MkButton>
|
||||
<MkTextarea v-model="script" class="_monospace" tall spellcheck="false">
|
||||
<MkCodeEditor v-model="script" lang="is">
|
||||
<template #label>{{ i18n.ts._play.script }}</template>
|
||||
</MkTextarea>
|
||||
</MkCodeEditor>
|
||||
<div class="_buttons">
|
||||
<MkButton primary @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
|
||||
<MkButton @click="show"><i class="ti ti-eye"></i> {{ i18n.ts.show }}</MkButton>
|
||||
@@ -40,6 +40,7 @@ import * as os from '@/os.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
import MkCodeEditor from '@/components/MkCodeEditor.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkSelect from '@/components/MkSelect.vue';
|
||||
import { useRouter } from '@/router.js';
|
||||
|
@@ -165,12 +165,8 @@ async function run() {
|
||||
return new Promise(ok => {
|
||||
os.inputText({
|
||||
title: q,
|
||||
}).then(({ canceled, result: a }) => {
|
||||
if (canceled) {
|
||||
ok('');
|
||||
} else {
|
||||
ok(a);
|
||||
}
|
||||
}).then(({ result: a }) => {
|
||||
ok(a ?? '');
|
||||
});
|
||||
});
|
||||
},
|
||||
|
@@ -60,6 +60,7 @@ async function create() {
|
||||
type: 'string',
|
||||
required: false,
|
||||
multiline: true,
|
||||
treatAsMfm: true,
|
||||
label: i18n.ts.description,
|
||||
},
|
||||
isPublic: {
|
||||
|
@@ -60,7 +60,7 @@ function setFilter(ev) {
|
||||
action: () => {
|
||||
includeTypes.value = null;
|
||||
},
|
||||
}, null, ...typeItems] : typeItems;
|
||||
}, { type: 'divider' }, ...typeItems] : typeItems;
|
||||
os.popupMenu(items, ev.currentTarget ?? ev.target);
|
||||
}
|
||||
|
||||
|
@@ -9,16 +9,17 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<template #header><i class="ti ti-align-left"></i> {{ i18n.ts._pages.blocks.text }}</template>
|
||||
|
||||
<section>
|
||||
<textarea v-model="text" :class="$style.textarea"></textarea>
|
||||
<textarea ref="inputEl" v-model="text" :class="$style.textarea"></textarea>
|
||||
</section>
|
||||
</XContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
/* eslint-disable vue/no-mutating-props */
|
||||
import { watch, ref } from 'vue';
|
||||
import { watch, ref, shallowRef, onMounted, onUnmounted } from 'vue';
|
||||
import XContainer from '../page-editor.container.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { Autocomplete } from '@/scripts/autocomplete.js';
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: any
|
||||
@@ -28,7 +29,10 @@ const emit = defineEmits<{
|
||||
(ev: 'update:modelValue', value: any): void;
|
||||
}>();
|
||||
|
||||
let autocomplete: Autocomplete;
|
||||
|
||||
const text = ref(props.modelValue.text ?? '');
|
||||
const inputEl = shallowRef<HTMLTextAreaElement | null>(null);
|
||||
|
||||
watch(text, () => {
|
||||
emit('update:modelValue', {
|
||||
@@ -36,6 +40,14 @@ watch(text, () => {
|
||||
text: text.value,
|
||||
});
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
autocomplete = new Autocomplete(inputEl.value, text);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
autocomplete.detach();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
@@ -26,9 +26,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</MkKeyValue>
|
||||
</FormSplit>
|
||||
|
||||
<MkTextarea v-model="valueForEditor" tall class="_monospace">
|
||||
<MkCodeEditor v-model="valueForEditor" lang="json5">
|
||||
<template #label>{{ i18n.ts.value }} (JSON)</template>
|
||||
</MkTextarea>
|
||||
</MkCodeEditor>
|
||||
|
||||
<MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
|
||||
|
||||
@@ -52,7 +52,7 @@ import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkKeyValue from '@/components/MkKeyValue.vue';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
import MkCodeEditor from '@/components/MkCodeEditor.vue';
|
||||
import FormSplit from '@/components/form/split.vue';
|
||||
import FormInfo from '@/components/MkInfo.vue';
|
||||
|
||||
|
@@ -7,15 +7,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<div class="_gaps_m">
|
||||
<FormInfo warn>{{ i18n.ts.customCssWarn }}</FormInfo>
|
||||
|
||||
<MkTextarea v-model="localCustomCss" manualSave tall class="_monospace" style="tab-size: 2;">
|
||||
<MkCodeEditor v-model="localCustomCss" manualSave lang="css">
|
||||
<template #label>CSS</template>
|
||||
</MkTextarea>
|
||||
</MkCodeEditor>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, computed } from 'vue';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
import MkCodeEditor from '@/components/MkCodeEditor.vue';
|
||||
import FormInfo from '@/components/MkInfo.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { unisonReload } from '@/scripts/unison-reload.js';
|
||||
|
274
packages/frontend/src/pages/settings/emoji-picker.vue
Normal file
274
packages/frontend/src/pages/settings/emoji-picker.vue
Normal file
@@ -0,0 +1,274 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="_gaps_m">
|
||||
<MkFolder :defaultOpen="true">
|
||||
<template #icon><i class="ti ti-pin"></i></template>
|
||||
<template #label>{{ i18n.ts.pinned }} ({{ i18n.ts.reaction }})</template>
|
||||
<template #caption>{{ i18n.ts.pinnedEmojisForReactionSettingDescription }}</template>
|
||||
|
||||
<div class="_gaps">
|
||||
<div>
|
||||
<div v-panel style="border-radius: 6px;">
|
||||
<Sortable
|
||||
v-model="pinnedEmojisForReaction"
|
||||
:class="$style.emojis"
|
||||
:itemKey="item => item"
|
||||
:animation="150"
|
||||
:delay="100"
|
||||
:delayOnTouchOnly="true"
|
||||
>
|
||||
<template #item="{element}">
|
||||
<button class="_button" :class="$style.emojisItem" @click="removeReaction(element, $event)">
|
||||
<MkCustomEmoji v-if="element[0] === ':'" :name="element" :normal="true"/>
|
||||
<MkEmoji v-else :emoji="element" :normal="true"/>
|
||||
</button>
|
||||
</template>
|
||||
<template #footer>
|
||||
<button class="_button" :class="$style.emojisAdd" @click="chooseReaction">
|
||||
<i class="ti ti-plus"></i>
|
||||
</button>
|
||||
</template>
|
||||
</Sortable>
|
||||
</div>
|
||||
<div :class="$style.editorCaption">{{ i18n.ts.reactionSettingDescription2 }}</div>
|
||||
</div>
|
||||
|
||||
<div class="_buttons">
|
||||
<MkButton inline @click="previewReaction"><i class="ti ti-eye"></i> {{ i18n.ts.preview }}</MkButton>
|
||||
<MkButton inline danger @click="setDefaultReaction"><i class="ti ti-reload"></i> {{ i18n.ts.default }}</MkButton>
|
||||
<MkButton inline danger @click="overwriteFromPinnedEmojis"><i class="ti ti-copy"></i> {{ i18n.ts.overwriteFromPinnedEmojis }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder>
|
||||
<template #icon><i class="ti ti-pin"></i></template>
|
||||
<template #label>{{ i18n.ts.pinned }} ({{ i18n.ts.general }})</template>
|
||||
<template #caption>{{ i18n.ts.pinnedEmojisSettingDescription }}</template>
|
||||
|
||||
<div class="_gaps">
|
||||
<div>
|
||||
<div v-panel style="border-radius: 6px;">
|
||||
<Sortable
|
||||
v-model="pinnedEmojis"
|
||||
:class="$style.emojis"
|
||||
:itemKey="item => item"
|
||||
:animation="150"
|
||||
:delay="100"
|
||||
:delayOnTouchOnly="true"
|
||||
>
|
||||
<template #item="{element}">
|
||||
<button class="_button" :class="$style.emojisItem" @click="removeEmoji(element, $event)">
|
||||
<MkCustomEmoji v-if="element[0] === ':'" :name="element" :normal="true"/>
|
||||
<MkEmoji v-else :emoji="element" :normal="true"/>
|
||||
</button>
|
||||
</template>
|
||||
<template #footer>
|
||||
<button class="_button" :class="$style.emojisAdd" @click="chooseEmoji">
|
||||
<i class="ti ti-plus"></i>
|
||||
</button>
|
||||
</template>
|
||||
</Sortable>
|
||||
</div>
|
||||
<div :class="$style.editorCaption">{{ i18n.ts.reactionSettingDescription2 }}</div>
|
||||
</div>
|
||||
|
||||
<div class="_buttons">
|
||||
<MkButton inline @click="previewEmoji"><i class="ti ti-eye"></i> {{ i18n.ts.preview }}</MkButton>
|
||||
<MkButton inline danger @click="setDefaultEmoji"><i class="ti ti-reload"></i> {{ i18n.ts.default }}</MkButton>
|
||||
<MkButton inline danger @click="overwriteFromPinnedEmojisForReaction"><i class="ti ti-copy"></i> {{ i18n.ts.overwriteFromPinnedEmojisForReaction }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<FormSection>
|
||||
<template #label>{{ i18n.ts.emojiPickerDisplay }}</template>
|
||||
|
||||
<div class="_gaps_m">
|
||||
<MkRadios v-model="emojiPickerScale">
|
||||
<template #label>{{ i18n.ts.size }}</template>
|
||||
<option :value="1">{{ i18n.ts.small }}</option>
|
||||
<option :value="2">{{ i18n.ts.medium }}</option>
|
||||
<option :value="3">{{ i18n.ts.large }}</option>
|
||||
</MkRadios>
|
||||
|
||||
<MkRadios v-model="emojiPickerWidth">
|
||||
<template #label>{{ i18n.ts.numberOfColumn }}</template>
|
||||
<option :value="1">5</option>
|
||||
<option :value="2">6</option>
|
||||
<option :value="3">7</option>
|
||||
<option :value="4">8</option>
|
||||
<option :value="5">9</option>
|
||||
</MkRadios>
|
||||
|
||||
<MkRadios v-model="emojiPickerHeight">
|
||||
<template #label>{{ i18n.ts.height }}</template>
|
||||
<option :value="1">{{ i18n.ts.small }}</option>
|
||||
<option :value="2">{{ i18n.ts.medium }}</option>
|
||||
<option :value="3">{{ i18n.ts.large }}</option>
|
||||
<option :value="4">{{ i18n.ts.large }}+</option>
|
||||
</MkRadios>
|
||||
|
||||
<MkSwitch v-model="emojiPickerUseDrawerForMobile">
|
||||
{{ i18n.ts.useDrawerReactionPickerForMobile }}
|
||||
<template #caption>{{ i18n.ts.needReloadToApply }}</template>
|
||||
</MkSwitch>
|
||||
</div>
|
||||
</FormSection>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, Ref, watch } from 'vue';
|
||||
import Sortable from 'vuedraggable';
|
||||
import MkRadios from '@/components/MkRadios.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { deepClone } from '@/scripts/clone.js';
|
||||
import { reactionPicker } from '@/scripts/reaction-picker.js';
|
||||
import { emojiPicker } from '@/scripts/emoji-picker.js';
|
||||
import MkCustomEmoji from '@/components/global/MkCustomEmoji.vue';
|
||||
import MkEmoji from '@/components/global/MkEmoji.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
|
||||
const pinnedEmojisForReaction: Ref<string[]> = ref(deepClone(defaultStore.state.reactions));
|
||||
const pinnedEmojis: Ref<string[]> = ref(deepClone(defaultStore.state.pinnedEmojis));
|
||||
|
||||
const emojiPickerScale = computed(defaultStore.makeGetterSetter('emojiPickerScale'));
|
||||
const emojiPickerWidth = computed(defaultStore.makeGetterSetter('emojiPickerWidth'));
|
||||
const emojiPickerHeight = computed(defaultStore.makeGetterSetter('emojiPickerHeight'));
|
||||
const emojiPickerUseDrawerForMobile = computed(defaultStore.makeGetterSetter('emojiPickerUseDrawerForMobile'));
|
||||
|
||||
const removeReaction = (reaction: string, ev: MouseEvent) => remove(pinnedEmojisForReaction, reaction, ev);
|
||||
const chooseReaction = (ev: MouseEvent) => pickEmoji(pinnedEmojisForReaction, ev);
|
||||
const setDefaultReaction = () => setDefault(pinnedEmojisForReaction);
|
||||
|
||||
const removeEmoji = (reaction: string, ev: MouseEvent) => remove(pinnedEmojis, reaction, ev);
|
||||
const chooseEmoji = (ev: MouseEvent) => pickEmoji(pinnedEmojis, ev);
|
||||
const setDefaultEmoji = () => setDefault(pinnedEmojis);
|
||||
|
||||
function previewReaction(ev: MouseEvent) {
|
||||
reactionPicker.show(getHTMLElement(ev));
|
||||
}
|
||||
|
||||
function previewEmoji(ev: MouseEvent) {
|
||||
emojiPicker.show(getHTMLElement(ev));
|
||||
}
|
||||
|
||||
async function overwriteFromPinnedEmojis() {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.ts.overwriteContentConfirm,
|
||||
});
|
||||
|
||||
if (canceled) {
|
||||
return;
|
||||
}
|
||||
|
||||
pinnedEmojisForReaction.value = [...pinnedEmojis.value];
|
||||
}
|
||||
|
||||
async function overwriteFromPinnedEmojisForReaction() {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.ts.overwriteContentConfirm,
|
||||
});
|
||||
|
||||
if (canceled) {
|
||||
return;
|
||||
}
|
||||
|
||||
pinnedEmojis.value = [...pinnedEmojisForReaction.value];
|
||||
}
|
||||
|
||||
function remove(itemsRef: Ref<string[]>, reaction: string, ev: MouseEvent) {
|
||||
os.popupMenu([{
|
||||
text: i18n.ts.remove,
|
||||
action: () => {
|
||||
itemsRef.value = itemsRef.value.filter(x => x !== reaction);
|
||||
},
|
||||
}], getHTMLElement(ev));
|
||||
}
|
||||
|
||||
async function setDefault(itemsRef: Ref<string[]>) {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.ts.resetAreYouSure,
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
itemsRef.value = deepClone(defaultStore.def.reactions.default);
|
||||
}
|
||||
|
||||
async function pickEmoji(itemsRef: Ref<string[]>, ev: MouseEvent) {
|
||||
os.pickEmoji(getHTMLElement(ev), {
|
||||
showPinned: false,
|
||||
}).then(it => {
|
||||
const emoji = it as string;
|
||||
if (!itemsRef.value.includes(emoji)) {
|
||||
itemsRef.value.push(emoji);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getHTMLElement(ev: MouseEvent): HTMLElement {
|
||||
const target = ev.currentTarget ?? ev.target;
|
||||
return target as HTMLElement;
|
||||
}
|
||||
|
||||
watch(pinnedEmojisForReaction, () => {
|
||||
defaultStore.set('reactions', pinnedEmojisForReaction.value);
|
||||
}, {
|
||||
deep: true,
|
||||
});
|
||||
|
||||
watch(pinnedEmojis, () => {
|
||||
defaultStore.set('pinnedEmojis', pinnedEmojis.value);
|
||||
}, {
|
||||
deep: true,
|
||||
});
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.emojiPicker,
|
||||
icon: 'ti ti-mood-happy',
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.tab {
|
||||
margin: calc(var(--margin) / 2) 0;
|
||||
padding: calc(var(--margin) / 2) 0;
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
.emojis {
|
||||
padding: 12px;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.emojisItem {
|
||||
display: inline-block;
|
||||
padding: 8px;
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.emojisAdd {
|
||||
display: inline-block;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.editorCaption {
|
||||
font-size: 0.85em;
|
||||
padding: 8px 0 0 0;
|
||||
color: var(--fgTransparentWeak);
|
||||
}
|
||||
</style>
|
@@ -123,6 +123,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkSwitch v-model="useSystemFont">{{ i18n.ts.useSystemFont }}</MkSwitch>
|
||||
<MkSwitch v-model="disableDrawer">{{ i18n.ts.disableDrawer }}</MkSwitch>
|
||||
<MkSwitch v-model="forceShowAds">{{ i18n.ts.forceShowAds }}</MkSwitch>
|
||||
<MkSwitch v-model="enableSeasonalScreenEffect">{{ i18n.ts.seasonalScreenEffect }}</MkSwitch>
|
||||
</div>
|
||||
<div>
|
||||
<MkRadios v-model="emojiStyle">
|
||||
@@ -291,11 +292,12 @@ const notificationStackAxis = computed(defaultStore.makeGetterSetter('notificati
|
||||
const keepScreenOn = computed(defaultStore.makeGetterSetter('keepScreenOn'));
|
||||
const disableStreamingTimeline = computed(defaultStore.makeGetterSetter('disableStreamingTimeline'));
|
||||
const useGroupedNotifications = computed(defaultStore.makeGetterSetter('useGroupedNotifications'));
|
||||
const enableSeasonalScreenEffect = computed(defaultStore.makeGetterSetter('enableSeasonalScreenEffect'));
|
||||
|
||||
watch(lang, () => {
|
||||
miLocalStorage.setItem('lang', lang.value as string);
|
||||
miLocalStorage.removeItem('locale');
|
||||
miLocalStorage.removeItem('localeVersion');
|
||||
miLocalStorage.removeItem('localeVersion');
|
||||
});
|
||||
|
||||
watch(fontSize, () => {
|
||||
@@ -331,6 +333,7 @@ watch([
|
||||
keepScreenOn,
|
||||
hideMutedNotes,
|
||||
disableStreamingTimeline,
|
||||
enableSeasonalScreenEffect,
|
||||
], async () => {
|
||||
await reloadAsk();
|
||||
});
|
||||
|
@@ -74,9 +74,9 @@ const menuDef = computed(() => [{
|
||||
active: currentPage.value?.route.name === 'privacy',
|
||||
}, {
|
||||
icon: 'ti ti-mood-happy',
|
||||
text: i18n.ts.reaction,
|
||||
to: '/settings/reaction',
|
||||
active: currentPage.value?.route.name === 'reaction',
|
||||
text: i18n.ts.emojiPicker,
|
||||
to: '/settings/emoji-picker',
|
||||
active: currentPage.value?.route.name === 'emojiPicker',
|
||||
}, {
|
||||
icon: 'ti ti-cloud',
|
||||
text: i18n.ts.drive,
|
||||
@@ -236,7 +236,7 @@ provideMetadataReceiver((info) => {
|
||||
childInfo.value = null;
|
||||
} else {
|
||||
childInfo.value = info;
|
||||
INFO.value.needWideArea = info.value?.needWideArea ?? undefined;
|
||||
INFO.value.needWideArea = info.value.needWideArea ?? undefined;
|
||||
}
|
||||
});
|
||||
|
||||
|
@@ -7,9 +7,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<div class="_gaps_m">
|
||||
<FormInfo warn>{{ i18n.ts._plugin.installWarn }}</FormInfo>
|
||||
|
||||
<MkTextarea v-model="code" tall>
|
||||
<MkCodeEditor v-model="code" lang="is">
|
||||
<template #label>{{ i18n.ts.code }}</template>
|
||||
</MkTextarea>
|
||||
</MkCodeEditor>
|
||||
|
||||
<div>
|
||||
<MkButton :disabled="code == null" primary inline @click="install"><i class="ti ti-check"></i> {{ i18n.ts.install }}</MkButton>
|
||||
@@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { nextTick, ref, computed } from 'vue';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
import MkCodeEditor from '@/components/MkCodeEditor.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import FormInfo from '@/components/MkInfo.vue';
|
||||
import * as os from '@/os.js';
|
||||
|
@@ -83,10 +83,10 @@ const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [
|
||||
'useReactionPickerForContextMenu',
|
||||
'showGapBetweenNotesInTimeline',
|
||||
'instanceTicker',
|
||||
'reactionPickerSize',
|
||||
'reactionPickerWidth',
|
||||
'reactionPickerHeight',
|
||||
'reactionPickerUseDrawerForMobile',
|
||||
'emojiPickerScale',
|
||||
'emojiPickerWidth',
|
||||
'emojiPickerHeight',
|
||||
'emojiPickerUseDrawerForMobile',
|
||||
'defaultSideView',
|
||||
'menuDisplay',
|
||||
'reportError',
|
||||
@@ -406,7 +406,7 @@ function menu(ev: MouseEvent, profileId: string) {
|
||||
icon: 'ti ti-download',
|
||||
href: URL.createObjectURL(new Blob([JSON.stringify(profiles.value[profileId], null, 2)], { type: 'application/json' })),
|
||||
download: `${profiles.value[profileId].name}.json`,
|
||||
}, null, {
|
||||
}, { type: 'divider' }, {
|
||||
text: ts.rename,
|
||||
icon: 'ti ti-forms',
|
||||
action: () => rename(profileId),
|
||||
@@ -414,7 +414,7 @@ function menu(ev: MouseEvent, profileId: string) {
|
||||
text: ts._preferencesBackups.save,
|
||||
icon: 'ti ti-device-floppy',
|
||||
action: () => save(profileId),
|
||||
}, null, {
|
||||
}, { type: 'divider' }, {
|
||||
text: ts.delete,
|
||||
icon: 'ti ti-trash',
|
||||
action: () => deleteProfile(profileId),
|
||||
|
@@ -1,114 +0,0 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<MkModalWindow
|
||||
ref="dialog"
|
||||
:width="400"
|
||||
:height="450"
|
||||
@close="cancel"
|
||||
@closed="emit('closed')"
|
||||
>
|
||||
<template #header>{{ i18n.ts.avatarDecorations }}</template>
|
||||
|
||||
<div>
|
||||
<MkSpacer :marginMin="20" :marginMax="28">
|
||||
<div style="text-align: center;">
|
||||
<div :class="$style.name">{{ decoration.name }}</div>
|
||||
<MkAvatar style="width: 64px; height: 64px; margin-bottom: 20px;" :user="$i" :decoration="{ url: decoration.url, angle, flipH }" forceShowDecoration/>
|
||||
</div>
|
||||
<div class="_gaps_s">
|
||||
<MkRange v-model="angle" continuousUpdate :min="-0.5" :max="0.5" :step="0.025" :textConverter="(v) => `${Math.floor(v * 360)}°`">
|
||||
<template #label>{{ i18n.ts.angle }}</template>
|
||||
</MkRange>
|
||||
<MkSwitch v-model="flipH">
|
||||
<template #label>{{ i18n.ts.flip }}</template>
|
||||
</MkSwitch>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
|
||||
<div :class="$style.footer" class="_buttonsCenter">
|
||||
<MkButton v-if="using" primary rounded @click="attach"><i class="ti ti-check"></i> {{ i18n.ts.update }}</MkButton>
|
||||
<MkButton v-if="using" rounded @click="detach"><i class="ti ti-x"></i> {{ i18n.ts.detach }}</MkButton>
|
||||
<MkButton v-else primary rounded @click="attach"><i class="ti ti-check"></i> {{ i18n.ts.attach }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</MkModalWindow>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { shallowRef, ref, computed } from 'vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import * as os from '@/os.js';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import MkRange from '@/components/MkRange.vue';
|
||||
import { $i } from '@/account.js';
|
||||
|
||||
const props = defineProps<{
|
||||
decoration: {
|
||||
id: string;
|
||||
url: string;
|
||||
}
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'closed'): void;
|
||||
}>();
|
||||
|
||||
const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
|
||||
const using = computed(() => $i.avatarDecorations.some(x => x.id === props.decoration.id));
|
||||
const angle = ref(using.value ? $i.avatarDecorations.find(x => x.id === props.decoration.id).angle ?? 0 : 0);
|
||||
const flipH = ref(using.value ? $i.avatarDecorations.find(x => x.id === props.decoration.id).flipH ?? false : false);
|
||||
|
||||
function cancel() {
|
||||
dialog.value.close();
|
||||
}
|
||||
|
||||
async function attach() {
|
||||
const decoration = {
|
||||
id: props.decoration.id,
|
||||
angle: angle.value,
|
||||
flipH: flipH.value,
|
||||
};
|
||||
await os.apiWithDialog('i/update', {
|
||||
avatarDecorations: [decoration],
|
||||
});
|
||||
$i.avatarDecorations = [decoration];
|
||||
|
||||
dialog.value.close();
|
||||
}
|
||||
|
||||
async function detach() {
|
||||
await os.apiWithDialog('i/update', {
|
||||
avatarDecorations: [],
|
||||
});
|
||||
$i.avatarDecorations = [];
|
||||
|
||||
dialog.value.close();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.name {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
font-weight: bold;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
padding: 12px;
|
||||
border-top: solid 0.5px var(--divider);
|
||||
-webkit-backdrop-filter: var(--blur, blur(15px));
|
||||
backdrop-filter: var(--blur, blur(15px));
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,69 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="[$style.root, { [$style.active]: active }]"
|
||||
@click="emit('click')"
|
||||
>
|
||||
<div :class="$style.name"><MkCondensedLine :minScale="0.5">{{ decoration.name }}</MkCondensedLine></div>
|
||||
<MkAvatar style="width: 60px; height: 60px;" :user="$i" :decorations="[{ url: decoration.url, angle, flipH, offsetX, offsetY }]" forceShowDecoration/>
|
||||
<i v-if="decoration.roleIdsThatCanBeUsedThisDecoration.length > 0 && !$i.roles.some(r => decoration.roleIdsThatCanBeUsedThisDecoration.includes(r.id))" :class="$style.lock" class="ti ti-lock"></i>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import { $i } from '@/account.js';
|
||||
|
||||
const props = defineProps<{
|
||||
active?: boolean;
|
||||
decoration: {
|
||||
id: string;
|
||||
url: string;
|
||||
name: string;
|
||||
roleIdsThatCanBeUsedThisDecoration: string[];
|
||||
};
|
||||
angle?: number;
|
||||
flipH?: boolean;
|
||||
offsetX?: number;
|
||||
offsetY?: number;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'click'): void;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
cursor: pointer;
|
||||
padding: 16px 16px 28px 16px;
|
||||
border: solid 2px var(--divider);
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
font-size: 90%;
|
||||
overflow: clip;
|
||||
contain: content;
|
||||
}
|
||||
|
||||
.active {
|
||||
background-color: var(--accentedBg);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.name {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
font-weight: bold;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.lock {
|
||||
position: absolute;
|
||||
bottom: 12px;
|
||||
right: 12px;
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,157 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<MkModalWindow
|
||||
ref="dialog"
|
||||
:width="400"
|
||||
:height="450"
|
||||
@close="cancel"
|
||||
@closed="emit('closed')"
|
||||
>
|
||||
<template #header>{{ i18n.ts.avatarDecorations }}</template>
|
||||
|
||||
<div>
|
||||
<MkSpacer :marginMin="20" :marginMax="28">
|
||||
<div style="text-align: center;">
|
||||
<div :class="$style.name">{{ decoration.name }}</div>
|
||||
<MkAvatar style="width: 64px; height: 64px; margin-bottom: 20px;" :user="$i" :decorations="decorationsForPreview" forceShowDecoration/>
|
||||
</div>
|
||||
<div class="_gaps_s">
|
||||
<MkRange v-model="angle" continuousUpdate :min="-0.5" :max="0.5" :step="0.025" :textConverter="(v) => `${Math.floor(v * 360)}°`">
|
||||
<template #label>{{ i18n.ts.angle }}</template>
|
||||
</MkRange>
|
||||
<MkRange v-model="offsetX" continuousUpdate :min="-0.25" :max="0.25" :step="0.025" :textConverter="(v) => `${Math.floor(v * 100)}%`">
|
||||
<template #label>X {{ i18n.ts.position }}</template>
|
||||
</MkRange>
|
||||
<MkRange v-model="offsetY" continuousUpdate :min="-0.25" :max="0.25" :step="0.025" :textConverter="(v) => `${Math.floor(v * 100)}%`">
|
||||
<template #label>Y {{ i18n.ts.position }}</template>
|
||||
</MkRange>
|
||||
<MkSwitch v-model="flipH">
|
||||
<template #label>{{ i18n.ts.flip }}</template>
|
||||
</MkSwitch>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
|
||||
<div :class="$style.footer" class="_buttonsCenter">
|
||||
<MkButton v-if="usingIndex != null" primary rounded @click="update"><i class="ti ti-check"></i> {{ i18n.ts.update }}</MkButton>
|
||||
<MkButton v-if="usingIndex != null" rounded @click="detach"><i class="ti ti-x"></i> {{ i18n.ts.detach }}</MkButton>
|
||||
<MkButton v-else :disabled="exceeded" primary rounded @click="attach"><i class="ti ti-check"></i> {{ i18n.ts.attach }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</MkModalWindow>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { shallowRef, ref, computed } from 'vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import * as os from '@/os.js';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import MkRange from '@/components/MkRange.vue';
|
||||
import { $i } from '@/account.js';
|
||||
|
||||
const props = defineProps<{
|
||||
usingIndex: number | null;
|
||||
decoration: {
|
||||
id: string;
|
||||
url: string;
|
||||
name: string;
|
||||
};
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'closed'): void;
|
||||
(ev: 'attach', payload: {
|
||||
angle: number;
|
||||
flipH: boolean;
|
||||
offsetX: number;
|
||||
offsetY: number;
|
||||
}): void;
|
||||
(ev: 'update', payload: {
|
||||
angle: number;
|
||||
flipH: boolean;
|
||||
offsetX: number;
|
||||
offsetY: number;
|
||||
}): void;
|
||||
(ev: 'detach'): void;
|
||||
}>();
|
||||
|
||||
const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
|
||||
const exceeded = computed(() => ($i.policies.avatarDecorationLimit - $i.avatarDecorations.length) <= 0);
|
||||
const angle = ref((props.usingIndex != null ? $i.avatarDecorations[props.usingIndex].angle : null) ?? 0);
|
||||
const flipH = ref((props.usingIndex != null ? $i.avatarDecorations[props.usingIndex].flipH : null) ?? false);
|
||||
const offsetX = ref((props.usingIndex != null ? $i.avatarDecorations[props.usingIndex].offsetX : null) ?? 0);
|
||||
const offsetY = ref((props.usingIndex != null ? $i.avatarDecorations[props.usingIndex].offsetY : null) ?? 0);
|
||||
|
||||
const decorationsForPreview = computed(() => {
|
||||
const decoration = {
|
||||
id: props.decoration.id,
|
||||
url: props.decoration.url,
|
||||
angle: angle.value,
|
||||
flipH: flipH.value,
|
||||
offsetX: offsetX.value,
|
||||
offsetY: offsetY.value,
|
||||
};
|
||||
const decorations = [...$i.avatarDecorations];
|
||||
if (props.usingIndex != null) {
|
||||
decorations[props.usingIndex] = decoration;
|
||||
} else {
|
||||
decorations.push(decoration);
|
||||
}
|
||||
return decorations;
|
||||
});
|
||||
|
||||
function cancel() {
|
||||
dialog.value.close();
|
||||
}
|
||||
|
||||
async function update() {
|
||||
emit('update', {
|
||||
angle: angle.value,
|
||||
flipH: flipH.value,
|
||||
offsetX: offsetX.value,
|
||||
offsetY: offsetY.value,
|
||||
});
|
||||
dialog.value.close();
|
||||
}
|
||||
|
||||
async function attach() {
|
||||
emit('attach', {
|
||||
angle: angle.value,
|
||||
flipH: flipH.value,
|
||||
offsetX: offsetX.value,
|
||||
offsetY: offsetY.value,
|
||||
});
|
||||
dialog.value.close();
|
||||
}
|
||||
|
||||
async function detach() {
|
||||
emit('detach');
|
||||
dialog.value.close();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.name {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
font-weight: bold;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
padding: 12px;
|
||||
border-top: solid 0.5px var(--divider);
|
||||
-webkit-backdrop-filter: var(--blur, blur(15px));
|
||||
backdrop-filter: var(--blur, blur(15px));
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,131 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div v-if="!loading" class="_gaps">
|
||||
<MkInfo>{{ i18n.t('_profile.avatarDecorationMax', { max: $i.policies.avatarDecorationLimit }) }} ({{ i18n.t('remainingN', { n: $i.policies.avatarDecorationLimit - $i.avatarDecorations.length }) }})</MkInfo>
|
||||
|
||||
<div v-if="$i.avatarDecorations.length > 0" v-panel :class="$style.current" class="_gaps_s">
|
||||
<div>{{ i18n.ts.inUse }}</div>
|
||||
|
||||
<div :class="$style.decorations">
|
||||
<XDecoration
|
||||
v-for="(avatarDecoration, i) in $i.avatarDecorations"
|
||||
:decoration="avatarDecorations.find(d => d.id === avatarDecoration.id)"
|
||||
:angle="avatarDecoration.angle"
|
||||
:flipH="avatarDecoration.flipH"
|
||||
:offsetX="avatarDecoration.offsetX"
|
||||
:offsetY="avatarDecoration.offsetY"
|
||||
:active="true"
|
||||
@click="openDecoration(avatarDecoration, i)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<MkButton danger @click="detachAllDecorations">{{ i18n.ts.detachAll }}</MkButton>
|
||||
</div>
|
||||
|
||||
<div :class="$style.decorations">
|
||||
<XDecoration
|
||||
v-for="avatarDecoration in avatarDecorations"
|
||||
:key="avatarDecoration.id"
|
||||
:decoration="avatarDecoration"
|
||||
@click="openDecoration(avatarDecoration)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<MkLoading/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, defineAsyncComponent } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import XDecoration from './profile.avatar-decoration.decoration.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { $i } from '@/account.js';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
|
||||
const loading = ref(true);
|
||||
const avatarDecorations = ref<Misskey.entities.GetAvatarDecorationsResponse>([]);
|
||||
|
||||
os.api('get-avatar-decorations').then(_avatarDecorations => {
|
||||
avatarDecorations.value = _avatarDecorations;
|
||||
loading.value = false;
|
||||
});
|
||||
|
||||
function openDecoration(avatarDecoration, index?: number) {
|
||||
os.popup(defineAsyncComponent(() => import('./profile.avatar-decoration.dialog.vue')), {
|
||||
decoration: avatarDecoration,
|
||||
usingIndex: index,
|
||||
}, {
|
||||
'attach': async (payload) => {
|
||||
const decoration = {
|
||||
id: avatarDecoration.id,
|
||||
angle: payload.angle,
|
||||
flipH: payload.flipH,
|
||||
offsetX: payload.offsetX,
|
||||
offsetY: payload.offsetY,
|
||||
};
|
||||
const update = [...$i.avatarDecorations, decoration];
|
||||
await os.apiWithDialog('i/update', {
|
||||
avatarDecorations: update,
|
||||
});
|
||||
$i.avatarDecorations = update;
|
||||
},
|
||||
'update': async (payload) => {
|
||||
const decoration = {
|
||||
id: avatarDecoration.id,
|
||||
angle: payload.angle,
|
||||
flipH: payload.flipH,
|
||||
offsetX: payload.offsetX,
|
||||
offsetY: payload.offsetY,
|
||||
};
|
||||
const update = [...$i.avatarDecorations];
|
||||
update[index] = decoration;
|
||||
await os.apiWithDialog('i/update', {
|
||||
avatarDecorations: update,
|
||||
});
|
||||
$i.avatarDecorations = update;
|
||||
},
|
||||
'detach': async () => {
|
||||
const update = [...$i.avatarDecorations];
|
||||
update.splice(index, 1);
|
||||
await os.apiWithDialog('i/update', {
|
||||
avatarDecorations: update,
|
||||
});
|
||||
$i.avatarDecorations = update;
|
||||
},
|
||||
}, 'closed');
|
||||
}
|
||||
|
||||
function detachAllDecorations() {
|
||||
os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.ts.areYouSure,
|
||||
}).then(async ({ canceled }) => {
|
||||
if (canceled) return;
|
||||
await os.apiWithDialog('i/update', {
|
||||
avatarDecorations: [],
|
||||
});
|
||||
$i.avatarDecorations = [];
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.current {
|
||||
padding: 16px;
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.decorations {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||
grid-gap: 12px;
|
||||
}
|
||||
</style>
|
@@ -13,11 +13,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkButton primary rounded :class="$style.bannerEdit" @click="changeBanner">{{ i18n.ts._profile.changeBanner }}</MkButton>
|
||||
</div>
|
||||
|
||||
<MkInput v-model="profile.name" :max="30" manualSave>
|
||||
<MkInput v-model="profile.name" :max="30" manualSave :mfmAutocomplete="['emoji']">
|
||||
<template #label>{{ i18n.ts._profile.name }}</template>
|
||||
</MkInput>
|
||||
|
||||
<MkTextarea v-model="profile.description" :max="500" tall manualSave>
|
||||
<MkTextarea v-model="profile.description" :max="500" tall manualSave mfmAutocomplete :mfmPreview="true">
|
||||
<template #label>{{ i18n.ts._profile.description }}</template>
|
||||
<template #caption>{{ i18n.ts._profile.youCanIncludeHashtags }}</template>
|
||||
</MkTextarea>
|
||||
@@ -87,18 +87,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<template #icon><i class="ti ti-sparkles"></i></template>
|
||||
<template #label>{{ i18n.ts.avatarDecorations }}</template>
|
||||
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); grid-gap: 12px;">
|
||||
<div
|
||||
v-for="avatarDecoration in avatarDecorations"
|
||||
:key="avatarDecoration.id"
|
||||
:class="[$style.avatarDecoration, { [$style.avatarDecorationActive]: $i.avatarDecorations.some(x => x.id === avatarDecoration.id) }]"
|
||||
@click="openDecoration(avatarDecoration)"
|
||||
>
|
||||
<div :class="$style.avatarDecorationName"><MkCondensedLine :minScale="0.5">{{ avatarDecoration.name }}</MkCondensedLine></div>
|
||||
<MkAvatar style="width: 60px; height: 60px;" :user="$i" :decoration="{ url: avatarDecoration.url }" forceShowDecoration/>
|
||||
<i v-if="avatarDecoration.roleIdsThatCanBeUsedThisDecoration.length > 0 && !$i.roles.some(r => avatarDecoration.roleIdsThatCanBeUsedThisDecoration.includes(r.id))" :class="$style.avatarDecorationLock" class="ti ti-lock"></i>
|
||||
</div>
|
||||
</div>
|
||||
<XAvatarDecoration/>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder>
|
||||
@@ -122,10 +111,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref, watch, defineAsyncComponent, onMounted, onUnmounted } from 'vue';
|
||||
import { computed, reactive, ref, watch, defineAsyncComponent } from 'vue';
|
||||
import Misskey from 'misskey-js';
|
||||
import XAvatarDecoration from './profile.avatar-decoration.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import MkSelect from '@/components/MkSelect.vue';
|
||||
import FormSplit from '@/components/form/split.vue';
|
||||
@@ -140,11 +130,11 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { claimAchievement } from '@/scripts/achievements.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
|
||||
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
|
||||
|
||||
const reactionAcceptance = computed(defaultStore.makeGetterSetter('reactionAcceptance'));
|
||||
const avatarDecorations = ref<any[]>([]);
|
||||
|
||||
const profile = reactive({
|
||||
name: $i.name,
|
||||
@@ -165,10 +155,6 @@ watch(() => profile, () => {
|
||||
const fields = ref($i?.fields.map(field => ({ id: Math.random().toString(), name: field.name, value: field.value })) ?? []);
|
||||
const fieldEditMode = ref(false);
|
||||
|
||||
os.api('get-avatar-decorations').then(_avatarDecorations => {
|
||||
avatarDecorations.value = _avatarDecorations;
|
||||
});
|
||||
|
||||
function addField() {
|
||||
fields.value.push({
|
||||
id: Math.random().toString(),
|
||||
@@ -267,12 +253,6 @@ function changeBanner(ev) {
|
||||
});
|
||||
}
|
||||
|
||||
function openDecoration(avatarDecoration) {
|
||||
os.popup(defineAsyncComponent(() => import('./profile.avatar-decoration-dialog.vue')), {
|
||||
decoration: avatarDecoration,
|
||||
}, {}, 'closed');
|
||||
}
|
||||
|
||||
const headerActions = computed(() => []);
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
@@ -367,33 +347,4 @@ definePageMetadata({
|
||||
.dragItemForm {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.avatarDecoration {
|
||||
cursor: pointer;
|
||||
padding: 16px 16px 28px 16px;
|
||||
border: solid 2px var(--divider);
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
font-size: 90%;
|
||||
overflow: clip;
|
||||
contain: content;
|
||||
}
|
||||
|
||||
.avatarDecorationActive {
|
||||
background-color: var(--accentedBg);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.avatarDecorationName {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
font-weight: bold;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.avatarDecorationLock {
|
||||
position: absolute;
|
||||
bottom: 12px;
|
||||
right: 12px;
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,159 +0,0 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="_gaps_m">
|
||||
<FromSlot>
|
||||
<template #label>{{ i18n.ts.reactionSettingDescription }}</template>
|
||||
<div v-panel style="border-radius: 6px;">
|
||||
<Sortable v-model="reactions" :class="$style.reactions" :itemKey="item => item" :animation="150" :delay="100" :delayOnTouchOnly="true">
|
||||
<template #item="{element}">
|
||||
<button class="_button" :class="$style.reactionsItem" @click="remove(element, $event)">
|
||||
<MkCustomEmoji v-if="element[0] === ':'" :name="element" :normal="true"/>
|
||||
<MkEmoji v-else :emoji="element" :normal="true"/>
|
||||
</button>
|
||||
</template>
|
||||
<template #footer>
|
||||
<button class="_button" :class="$style.reactionsAdd" @click="chooseEmoji"><i class="ti ti-plus"></i></button>
|
||||
</template>
|
||||
</Sortable>
|
||||
</div>
|
||||
<template #caption>{{ i18n.ts.reactionSettingDescription2 }} <button class="_textButton" @click="preview">{{ i18n.ts.preview }}</button></template>
|
||||
</FromSlot>
|
||||
|
||||
<MkRadios v-model="reactionPickerSize">
|
||||
<template #label>{{ i18n.ts.size }}</template>
|
||||
<option :value="1">{{ i18n.ts.small }}</option>
|
||||
<option :value="2">{{ i18n.ts.medium }}</option>
|
||||
<option :value="3">{{ i18n.ts.large }}</option>
|
||||
</MkRadios>
|
||||
<MkRadios v-model="reactionPickerWidth">
|
||||
<template #label>{{ i18n.ts.numberOfColumn }}</template>
|
||||
<option :value="1">5</option>
|
||||
<option :value="2">6</option>
|
||||
<option :value="3">7</option>
|
||||
<option :value="4">8</option>
|
||||
<option :value="5">9</option>
|
||||
</MkRadios>
|
||||
<MkRadios v-model="reactionPickerHeight">
|
||||
<template #label>{{ i18n.ts.height }}</template>
|
||||
<option :value="1">{{ i18n.ts.small }}</option>
|
||||
<option :value="2">{{ i18n.ts.medium }}</option>
|
||||
<option :value="3">{{ i18n.ts.large }}</option>
|
||||
<option :value="4">{{ i18n.ts.large }}+</option>
|
||||
</MkRadios>
|
||||
|
||||
<MkSwitch v-model="reactionPickerUseDrawerForMobile">
|
||||
{{ i18n.ts.useDrawerReactionPickerForMobile }}
|
||||
<template #caption>{{ i18n.ts.needReloadToApply }}</template>
|
||||
</MkSwitch>
|
||||
|
||||
<FormSection>
|
||||
<div class="_buttons">
|
||||
<MkButton inline @click="preview"><i class="ti ti-eye"></i> {{ i18n.ts.preview }}</MkButton>
|
||||
<MkButton inline danger @click="setDefault"><i class="ti ti-reload"></i> {{ i18n.ts.default }}</MkButton>
|
||||
</div>
|
||||
</FormSection>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, watch, ref, computed } from 'vue';
|
||||
import Sortable from 'vuedraggable';
|
||||
import MkRadios from '@/components/MkRadios.vue';
|
||||
import FromSlot from '@/components/form/slot.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { deepClone } from '@/scripts/clone.js';
|
||||
|
||||
const reactions = ref(deepClone(defaultStore.state.reactions));
|
||||
|
||||
const reactionPickerSize = computed(defaultStore.makeGetterSetter('reactionPickerSize'));
|
||||
const reactionPickerWidth = computed(defaultStore.makeGetterSetter('reactionPickerWidth'));
|
||||
const reactionPickerHeight = computed(defaultStore.makeGetterSetter('reactionPickerHeight'));
|
||||
const reactionPickerUseDrawerForMobile = computed(defaultStore.makeGetterSetter('reactionPickerUseDrawerForMobile'));
|
||||
|
||||
function save() {
|
||||
defaultStore.set('reactions', reactions.value);
|
||||
}
|
||||
|
||||
function remove(reaction, ev: MouseEvent) {
|
||||
os.popupMenu([{
|
||||
text: i18n.ts.remove,
|
||||
action: () => {
|
||||
reactions.value = reactions.value.filter(x => x !== reaction);
|
||||
},
|
||||
}], ev.currentTarget ?? ev.target);
|
||||
}
|
||||
|
||||
function preview(ev: MouseEvent) {
|
||||
os.popup(defineAsyncComponent(() => import('@/components/MkEmojiPickerDialog.vue')), {
|
||||
asReactionPicker: true,
|
||||
src: ev.currentTarget ?? ev.target,
|
||||
}, {}, 'closed');
|
||||
}
|
||||
|
||||
async function setDefault() {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.ts.resetAreYouSure,
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
reactions.value = deepClone(defaultStore.def.reactions.default);
|
||||
}
|
||||
|
||||
function chooseEmoji(ev: MouseEvent) {
|
||||
os.pickEmoji(ev.currentTarget ?? ev.target, {
|
||||
showPinned: false,
|
||||
}).then(emoji => {
|
||||
if (!reactions.value.includes(emoji)) {
|
||||
reactions.value.push(emoji);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
watch(reactions, () => {
|
||||
save();
|
||||
}, {
|
||||
deep: true,
|
||||
});
|
||||
|
||||
const headerActions = computed(() => []);
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.reaction,
|
||||
icon: 'ti ti-mood-happy',
|
||||
action: {
|
||||
icon: 'ti ti-eye',
|
||||
handler: preview,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.reactions {
|
||||
padding: 12px;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.reactionsItem {
|
||||
display: inline-block;
|
||||
padding: 8px;
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.reactionsAdd {
|
||||
display: inline-block;
|
||||
padding: 8px;
|
||||
}
|
||||
</style>
|
@@ -5,9 +5,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<template>
|
||||
<div class="_gaps_m">
|
||||
<MkTextarea v-model="installThemeCode">
|
||||
<MkCodeEditor v-model="installThemeCode" lang="json5">
|
||||
<template #label>{{ i18n.ts._theme.code }}</template>
|
||||
</MkTextarea>
|
||||
</MkCodeEditor>
|
||||
|
||||
<div class="_buttons">
|
||||
<MkButton :disabled="installThemeCode == null" inline @click="() => previewTheme(installThemeCode)"><i class="ti ti-eye"></i> {{ i18n.ts.preview }}</MkButton>
|
||||
@@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
import MkCodeEditor from '@/components/MkCodeEditor.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { parseThemeCode, previewTheme, installTheme } from '@/scripts/install-theme.js';
|
||||
import * as os from '@/os.js';
|
||||
|
@@ -51,9 +51,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<template #label>{{ i18n.ts.editCode }}</template>
|
||||
|
||||
<div class="_gaps_m">
|
||||
<MkTextarea v-model="themeCode" tall>
|
||||
<MkCodeEditor v-model="themeCode" lang="json5">
|
||||
<template #label>{{ i18n.ts._theme.code }}</template>
|
||||
</MkTextarea>
|
||||
</MkCodeEditor>
|
||||
<MkButton primary @click="applyThemeCode">{{ i18n.ts.apply }}</MkButton>
|
||||
</div>
|
||||
</MkFolder>
|
||||
@@ -80,6 +80,7 @@ import { v4 as uuid } from 'uuid';
|
||||
import JSON5 from 'json5';
|
||||
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkCodeEditor from '@/components/MkCodeEditor.vue';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
|
||||
|
@@ -48,6 +48,7 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { antennasCache, userListsCache } from '@/cache.js';
|
||||
import { deviceKind } from '@/scripts/device-kind.js';
|
||||
import { MenuItem } from '@/types/menu.js';
|
||||
|
||||
provide('shouldOmitHeaderTitle', true);
|
||||
|
||||
@@ -83,22 +84,40 @@ function top(): void {
|
||||
|
||||
async function chooseList(ev: MouseEvent): Promise<void> {
|
||||
const lists = await userListsCache.fetch();
|
||||
const items = lists.map(list => ({
|
||||
type: 'link' as const,
|
||||
text: list.name,
|
||||
to: `/timeline/list/${list.id}`,
|
||||
}));
|
||||
const items: MenuItem[] = [
|
||||
...lists.map(list => ({
|
||||
type: 'link' as const,
|
||||
text: list.name,
|
||||
to: `/timeline/list/${list.id}`,
|
||||
})),
|
||||
(lists.length === 0 ? undefined : { type: 'divider' }),
|
||||
{
|
||||
type: 'link' as const,
|
||||
icon: 'ti ti-plus',
|
||||
text: i18n.ts.createNew,
|
||||
to: '/my/lists',
|
||||
},
|
||||
];
|
||||
os.popupMenu(items, ev.currentTarget ?? ev.target);
|
||||
}
|
||||
|
||||
async function chooseAntenna(ev: MouseEvent): Promise<void> {
|
||||
const antennas = await antennasCache.fetch();
|
||||
const items = antennas.map(antenna => ({
|
||||
type: 'link' as const,
|
||||
text: antenna.name,
|
||||
indicate: antenna.hasUnreadNote,
|
||||
to: `/timeline/antenna/${antenna.id}`,
|
||||
}));
|
||||
const items: MenuItem[] = [
|
||||
...antennas.map(antenna => ({
|
||||
type: 'link' as const,
|
||||
text: antenna.name,
|
||||
indicate: antenna.hasUnreadNote,
|
||||
to: `/timeline/antenna/${antenna.id}`,
|
||||
})),
|
||||
(antennas.length === 0 ? undefined : { type: 'divider' }),
|
||||
{
|
||||
type: 'link' as const,
|
||||
icon: 'ti ti-plus',
|
||||
text: i18n.ts.createNew,
|
||||
to: '/my/antennas',
|
||||
},
|
||||
];
|
||||
os.popupMenu(items, ev.currentTarget ?? ev.target);
|
||||
}
|
||||
|
||||
@@ -106,12 +125,21 @@ async function chooseChannel(ev: MouseEvent): Promise<void> {
|
||||
const channels = await os.api('channels/my-favorites', {
|
||||
limit: 100,
|
||||
});
|
||||
const items = channels.map(channel => ({
|
||||
type: 'link' as const,
|
||||
text: channel.name,
|
||||
indicate: channel.hasUnreadNote,
|
||||
to: `/channels/${channel.id}`,
|
||||
}));
|
||||
const items: MenuItem[] = [
|
||||
...channels.map(channel => ({
|
||||
type: 'link' as const,
|
||||
text: channel.name,
|
||||
indicate: channel.hasUnreadNote,
|
||||
to: `/channels/${channel.id}`,
|
||||
})),
|
||||
(channels.length === 0 ? undefined : { type: 'divider' }),
|
||||
{
|
||||
type: 'link' as const,
|
||||
icon: 'ti ti-plus',
|
||||
text: i18n.ts.createNew,
|
||||
to: '/channels',
|
||||
},
|
||||
];
|
||||
os.popupMenu(items, ev.currentTarget ?? ev.target);
|
||||
}
|
||||
|
||||
|
@@ -128,12 +128,17 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</div>
|
||||
<MkInfo v-else-if="$i && $i.id === user.id">{{ i18n.ts.userPagePinTip }}</MkInfo>
|
||||
<template v-if="narrow">
|
||||
<XFiles :key="user.id" :user="user"/>
|
||||
<XActivity :key="user.id" :user="user"/>
|
||||
<MkLazy>
|
||||
<XFiles :key="user.id" :user="user"/>
|
||||
</MkLazy>
|
||||
<MkLazy>
|
||||
<XActivity :key="user.id" :user="user"/>
|
||||
</MkLazy>
|
||||
</template>
|
||||
<div v-if="!disableNotes">
|
||||
<div style="margin-bottom: 8px;">{{ i18n.ts.featured }}</div>
|
||||
<MkNotes :class="$style.tl" :noGap="true" :pagination="pagination"/>
|
||||
<MkLazy>
|
||||
<XTimeline :user="user"/>
|
||||
</MkLazy>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -187,6 +192,7 @@ function calcAge(birthdate: string): number {
|
||||
|
||||
const XFiles = defineAsyncComponent(() => import('./index.files.vue'));
|
||||
const XActivity = defineAsyncComponent(() => import('./index.activity.vue'));
|
||||
const XTimeline = defineAsyncComponent(() => import('./index.timeline.vue'));
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
user: Misskey.entities.UserDetailed;
|
||||
@@ -213,14 +219,6 @@ watch(moderationNote, async () => {
|
||||
await os.api('admin/update-user-note', { userId: props.user.id, text: moderationNote.value });
|
||||
});
|
||||
|
||||
const pagination = {
|
||||
endpoint: 'users/featured-notes' as const,
|
||||
limit: 10,
|
||||
params: computed(() => ({
|
||||
userId: props.user.id,
|
||||
})),
|
||||
};
|
||||
|
||||
const style = computed(() => {
|
||||
if (props.user.bannerUrl == null) return {};
|
||||
return {
|
||||
@@ -232,7 +230,7 @@ const age = computed(() => {
|
||||
return calcAge(props.user.birthday);
|
||||
});
|
||||
|
||||
function menu(ev) {
|
||||
function menu(ev: MouseEvent) {
|
||||
const { menu, cleanup } = getUserMenu(user.value, router);
|
||||
os.popupMenu(menu, ev.currentTarget ?? ev.target).finally(cleanup);
|
||||
}
|
||||
|
@@ -4,18 +4,17 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<MkSpacer :contentMax="800" style="padding-top: 0">
|
||||
<MkStickyContainer>
|
||||
<template #header>
|
||||
<MkTab v-model="include" :class="$style.tab">
|
||||
<option :value="null">{{ i18n.ts.notes }}</option>
|
||||
<option value="all">{{ i18n.ts.all }}</option>
|
||||
<option value="files">{{ i18n.ts.withFiles }}</option>
|
||||
</MkTab>
|
||||
</template>
|
||||
<MkNotes :noGap="true" :pagination="pagination" :class="$style.tl"/>
|
||||
</MkStickyContainer>
|
||||
</MkSpacer>
|
||||
<MkStickyContainer>
|
||||
<template #header>
|
||||
<MkTab v-model="tab" :class="$style.tab">
|
||||
<option value="featured">{{ i18n.ts.featured }}</option>
|
||||
<option :value="null">{{ i18n.ts.notes }}</option>
|
||||
<option value="all">{{ i18n.ts.all }}</option>
|
||||
<option value="files">{{ i18n.ts.withFiles }}</option>
|
||||
</MkTab>
|
||||
</template>
|
||||
<MkNotes :noGap="true" :pagination="pagination" :class="$style.tl"/>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@@ -29,24 +28,29 @@ const props = defineProps<{
|
||||
user: Misskey.entities.UserDetailed;
|
||||
}>();
|
||||
|
||||
const include = ref<string | null>('all');
|
||||
const tab = ref<string | null>('all');
|
||||
|
||||
const pagination = {
|
||||
const pagination = computed(() => tab.value === 'featured' ? {
|
||||
endpoint: 'users/featured-notes' as const,
|
||||
limit: 10,
|
||||
params: {
|
||||
userId: props.user.id,
|
||||
},
|
||||
} : {
|
||||
endpoint: 'users/notes' as const,
|
||||
limit: 10,
|
||||
params: computed(() => ({
|
||||
params: {
|
||||
userId: props.user.id,
|
||||
withRenotes: include.value === 'all',
|
||||
withReplies: include.value === 'all',
|
||||
withFiles: include.value === 'files',
|
||||
withRenotes: tab.value === 'all',
|
||||
withReplies: tab.value === 'all',
|
||||
withFiles: tab.value === 'files',
|
||||
withChannelNotes: true,
|
||||
})),
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.tab {
|
||||
margin: calc(var(--margin) / 2) 0;
|
||||
padding: calc(var(--margin) / 2) 0;
|
||||
background: var(--bg);
|
||||
}
|
||||
|
@@ -9,7 +9,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<div>
|
||||
<div v-if="user">
|
||||
<XHome v-if="tab === 'home'" :user="user"/>
|
||||
<XTimeline v-else-if="tab === 'notes'" :user="user"/>
|
||||
<MkSpacer v-else-if="tab === 'notes'" :contentMax="800" style="padding-top: 0">
|
||||
<XTimeline :user="user"/>
|
||||
</MkSpacer>
|
||||
<XActivity v-else-if="tab === 'activity'" :user="user"/>
|
||||
<XAchievements v-else-if="tab === 'achievements'" :user="user"/>
|
||||
<XReactions v-else-if="tab === 'reactions'" :user="user"/>
|
||||
|
@@ -63,9 +63,9 @@ export const routes = [{
|
||||
name: 'privacy',
|
||||
component: page(() => import('./pages/settings/privacy.vue')),
|
||||
}, {
|
||||
path: '/reaction',
|
||||
name: 'reaction',
|
||||
component: page(() => import('./pages/settings/reaction.vue')),
|
||||
path: '/emoji-picker',
|
||||
name: 'emojiPicker',
|
||||
component: page(() => import('./pages/settings/emoji-picker.vue')),
|
||||
}, {
|
||||
path: '/drive',
|
||||
name: 'drive',
|
||||
|
@@ -8,6 +8,8 @@ import getCaretCoordinates from 'textarea-caret';
|
||||
import { toASCII } from 'punycode/';
|
||||
import { popup } from '@/os.js';
|
||||
|
||||
export type SuggestionType = 'user' | 'hashtag' | 'emoji' | 'mfmTag';
|
||||
|
||||
export class Autocomplete {
|
||||
private suggestion: {
|
||||
x: Ref<number>;
|
||||
@@ -19,6 +21,7 @@ export class Autocomplete {
|
||||
private currentType: string;
|
||||
private textRef: Ref<string>;
|
||||
private opening: boolean;
|
||||
private onlyType: SuggestionType[];
|
||||
|
||||
private get text(): string {
|
||||
// Use raw .value to get the latest value
|
||||
@@ -35,7 +38,7 @@ export class Autocomplete {
|
||||
/**
|
||||
* 対象のテキストエリアを与えてインスタンスを初期化します。
|
||||
*/
|
||||
constructor(textarea: HTMLInputElement | HTMLTextAreaElement, textRef: Ref<string>) {
|
||||
constructor(textarea: HTMLInputElement | HTMLTextAreaElement, textRef: Ref<string>, onlyType?: SuggestionType[]) {
|
||||
//#region BIND
|
||||
this.onInput = this.onInput.bind(this);
|
||||
this.complete = this.complete.bind(this);
|
||||
@@ -46,6 +49,7 @@ export class Autocomplete {
|
||||
this.textarea = textarea;
|
||||
this.textRef = textRef;
|
||||
this.opening = false;
|
||||
this.onlyType = onlyType ?? ['user', 'hashtag', 'emoji', 'mfmTag'];
|
||||
|
||||
this.attach();
|
||||
}
|
||||
@@ -95,7 +99,7 @@ export class Autocomplete {
|
||||
|
||||
let opened = false;
|
||||
|
||||
if (isMention) {
|
||||
if (isMention && this.onlyType.includes('user')) {
|
||||
const username = text.substring(mentionIndex + 1);
|
||||
if (username !== '' && username.match(/^[a-zA-Z0-9_]+$/)) {
|
||||
this.open('user', username);
|
||||
@@ -106,7 +110,7 @@ export class Autocomplete {
|
||||
}
|
||||
}
|
||||
|
||||
if (isHashtag && !opened) {
|
||||
if (isHashtag && !opened && this.onlyType.includes('hashtag')) {
|
||||
const hashtag = text.substring(hashtagIndex + 1);
|
||||
if (!hashtag.includes(' ')) {
|
||||
this.open('hashtag', hashtag);
|
||||
@@ -114,7 +118,7 @@ export class Autocomplete {
|
||||
}
|
||||
}
|
||||
|
||||
if (isEmoji && !opened) {
|
||||
if (isEmoji && !opened && this.onlyType.includes('emoji')) {
|
||||
const emoji = text.substring(emojiIndex + 1);
|
||||
if (!emoji.includes(' ')) {
|
||||
this.open('emoji', emoji);
|
||||
@@ -122,7 +126,7 @@ export class Autocomplete {
|
||||
}
|
||||
}
|
||||
|
||||
if (isMfmTag && !opened) {
|
||||
if (isMfmTag && !opened && this.onlyType.includes('mfmTag')) {
|
||||
const mfmTag = text.substring(mfmTagIndex + 1);
|
||||
if (!mfmTag.includes(' ')) {
|
||||
this.open('mfmTag', mfmTag.replace('[', ''));
|
||||
|
@@ -6,6 +6,7 @@ import { fetchCustomEmojis } from '@/custom-emojis.js';
|
||||
export async function clearCache() {
|
||||
os.waiting();
|
||||
miLocalStorage.removeItem('locale');
|
||||
miLocalStorage.removeItem('localeVersion');
|
||||
miLocalStorage.removeItem('theme');
|
||||
miLocalStorage.removeItem('emojis');
|
||||
miLocalStorage.removeItem('lastEmojisFetchedAt');
|
||||
|
@@ -3,8 +3,9 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineAsyncComponent, Ref, ref } from 'vue';
|
||||
import { defineAsyncComponent, Ref, ref, computed, ComputedRef } from 'vue';
|
||||
import { popup } from '@/os.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
|
||||
/**
|
||||
* 絵文字ピッカーを表示する。
|
||||
@@ -23,8 +24,10 @@ class EmojiPicker {
|
||||
}
|
||||
|
||||
public async init() {
|
||||
const emojisRef = defaultStore.reactiveState.pinnedEmojis;
|
||||
await popup(defineAsyncComponent(() => import('@/components/MkEmojiPickerDialog.vue')), {
|
||||
src: this.src,
|
||||
pinnedEmojis: emojisRef,
|
||||
asReactionPicker: false,
|
||||
manualShowing: this.manualShowing,
|
||||
choseAndClose: false,
|
||||
@@ -44,8 +47,8 @@ class EmojiPicker {
|
||||
|
||||
public show(
|
||||
src: HTMLElement,
|
||||
onChosen: EmojiPicker['onChosen'],
|
||||
onClosed: EmojiPicker['onClosed'],
|
||||
onChosen?: EmojiPicker['onChosen'],
|
||||
onClosed?: EmojiPicker['onClosed'],
|
||||
) {
|
||||
this.src.value = src;
|
||||
this.manualShowing.value = true;
|
||||
|
@@ -82,7 +82,7 @@ export function getDriveFileMenu(file: Misskey.entities.DriveFile, folder?: Miss
|
||||
to: `/my/drive/file/${file.id}`,
|
||||
text: i18n.ts._fileViewer.title,
|
||||
icon: 'ti ti-info-circle',
|
||||
}, null, {
|
||||
}, { type: 'divider' }, {
|
||||
text: i18n.ts.rename,
|
||||
icon: 'ti ti-forms',
|
||||
action: () => rename(file),
|
||||
@@ -101,7 +101,7 @@ export function getDriveFileMenu(file: Misskey.entities.DriveFile, folder?: Miss
|
||||
aspectRatio: NaN,
|
||||
uploadFolder: folder ? folder.id : folder,
|
||||
}),
|
||||
}] : [], null, {
|
||||
}] : [], { type: 'divider' }, {
|
||||
text: i18n.ts.createNoteFromTheFile,
|
||||
icon: 'ti ti-pencil',
|
||||
action: () => os.post({
|
||||
@@ -118,7 +118,7 @@ export function getDriveFileMenu(file: Misskey.entities.DriveFile, folder?: Miss
|
||||
text: i18n.ts.download,
|
||||
icon: 'ti ti-download',
|
||||
download: file.name,
|
||||
}, null, {
|
||||
}, { type: 'divider' }, {
|
||||
text: i18n.ts.delete,
|
||||
icon: 'ti ti-trash',
|
||||
danger: true,
|
||||
@@ -126,7 +126,7 @@ export function getDriveFileMenu(file: Misskey.entities.DriveFile, folder?: Miss
|
||||
}];
|
||||
|
||||
if (defaultStore.state.devMode) {
|
||||
menu = menu.concat([null, {
|
||||
menu = menu.concat([{ type: 'divider' }, {
|
||||
icon: 'ti ti-id',
|
||||
text: i18n.ts.copyFileId,
|
||||
action: () => {
|
||||
|
@@ -61,7 +61,7 @@ export async function getNoteClipMenu(props: {
|
||||
},
|
||||
);
|
||||
},
|
||||
})), null, {
|
||||
})), { type: 'divider' }, {
|
||||
icon: 'ti ti-plus',
|
||||
text: i18n.ts.createNew,
|
||||
action: async () => {
|
||||
@@ -94,7 +94,7 @@ export async function getNoteClipMenu(props: {
|
||||
}];
|
||||
}
|
||||
|
||||
export function getAbuseNoteMenu(note: misskey.entities.Note, text: string): MenuItem {
|
||||
export function getAbuseNoteMenu(note: Misskey.entities.Note, text: string): MenuItem {
|
||||
return {
|
||||
icon: 'ti ti-exclamation-circle',
|
||||
text,
|
||||
@@ -108,7 +108,7 @@ export function getAbuseNoteMenu(note: misskey.entities.Note, text: string): Men
|
||||
};
|
||||
}
|
||||
|
||||
export function getCopyNoteLinkMenu(note: misskey.entities.Note, text: string): MenuItem {
|
||||
export function getCopyNoteLinkMenu(note: Misskey.entities.Note, text: string): MenuItem {
|
||||
return {
|
||||
icon: 'ti ti-link',
|
||||
text,
|
||||
@@ -264,7 +264,7 @@ export function getNoteMenu(props: {
|
||||
text: i18n.ts.unclip,
|
||||
danger: true,
|
||||
action: unclip,
|
||||
}, null] : []
|
||||
}, { type: 'divider' }] : []
|
||||
), {
|
||||
icon: 'ti ti-info-circle',
|
||||
text: i18n.ts.details,
|
||||
@@ -291,7 +291,7 @@ export function getNoteMenu(props: {
|
||||
text: i18n.ts.translate,
|
||||
action: translate,
|
||||
} : undefined,
|
||||
null,
|
||||
{ type: 'divider' },
|
||||
statePromise.then(state => state.isFavorited ? {
|
||||
icon: 'ti ti-star-off',
|
||||
text: i18n.ts.unfavorite,
|
||||
@@ -338,7 +338,7 @@ export function getNoteMenu(props: {
|
||||
},
|
||||
/*
|
||||
...($i.isModerator || $i.isAdmin ? [
|
||||
null,
|
||||
{ type: 'divider' },
|
||||
{
|
||||
icon: 'ti ti-speakerphone',
|
||||
text: i18n.ts.promote,
|
||||
@@ -347,13 +347,13 @@ export function getNoteMenu(props: {
|
||||
: []
|
||||
),*/
|
||||
...(appearNote.userId !== $i.id ? [
|
||||
null,
|
||||
{ type: 'divider' },
|
||||
appearNote.userId !== $i.id ? getAbuseNoteMenu(appearNote, i18n.ts.reportAbuse) : undefined,
|
||||
]
|
||||
: []
|
||||
),
|
||||
...(appearNote.userId === $i.id || $i.isModerator || $i.isAdmin ? [
|
||||
null,
|
||||
{ type: 'divider' },
|
||||
appearNote.userId === $i.id ? {
|
||||
icon: 'ti ti-edit',
|
||||
text: i18n.ts.deleteAndEdit,
|
||||
@@ -389,7 +389,7 @@ export function getNoteMenu(props: {
|
||||
}
|
||||
|
||||
if (noteActions.length > 0) {
|
||||
menu = menu.concat([null, ...noteActions.map(action => ({
|
||||
menu = menu.concat([{ type: "divider" }, ...noteActions.map(action => ({
|
||||
icon: 'ti ti-plug',
|
||||
text: action.title,
|
||||
action: () => {
|
||||
@@ -399,7 +399,7 @@ export function getNoteMenu(props: {
|
||||
}
|
||||
|
||||
if (defaultStore.state.devMode) {
|
||||
menu = menu.concat([null, {
|
||||
menu = menu.concat([{ type: "divider" }, {
|
||||
icon: 'ti ti-id',
|
||||
text: i18n.ts.copyNoteId,
|
||||
action: () => {
|
||||
@@ -528,10 +528,9 @@ export function getRenoteMenu(props: {
|
||||
}]);
|
||||
}
|
||||
|
||||
// nullを挟むことで区切り線を出せる
|
||||
const renoteItems = [
|
||||
...normalRenoteItems,
|
||||
...(channelRenoteItems.length > 0 && normalRenoteItems.length > 0) ? [null] : [],
|
||||
...(channelRenoteItems.length > 0 && normalRenoteItems.length > 0) ? [{ type: 'divider' }] : [],
|
||||
...channelRenoteItems,
|
||||
];
|
||||
|
||||
|
@@ -119,7 +119,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router
|
||||
userId: user.id,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
async function invalidateFollow() {
|
||||
if (!await getConfirmed(i18n.ts.breakFollowConfirm)) return;
|
||||
|
||||
@@ -183,7 +183,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router
|
||||
const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${user.host}`;
|
||||
os.post({ specified: user, initialText: `${canonical} ` });
|
||||
},
|
||||
}, null, {
|
||||
}, { type: 'divider' }, {
|
||||
icon: 'ti ti-pencil',
|
||||
text: i18n.ts.editMemo,
|
||||
action: () => {
|
||||
@@ -307,7 +307,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router
|
||||
}]);
|
||||
//}
|
||||
|
||||
menu = menu.concat([null, {
|
||||
menu = menu.concat([{ type: 'divider' }, {
|
||||
icon: user.isMuted ? 'ti ti-eye' : 'ti ti-eye-off',
|
||||
text: user.isMuted ? i18n.ts.unmute : i18n.ts.mute,
|
||||
action: toggleMute,
|
||||
@@ -329,7 +329,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router
|
||||
}]);
|
||||
}
|
||||
|
||||
menu = menu.concat([null, {
|
||||
menu = menu.concat([{ type: 'divider' }, {
|
||||
icon: 'ti ti-exclamation-circle',
|
||||
text: i18n.ts.reportAbuse,
|
||||
action: reportAbuse,
|
||||
@@ -337,15 +337,15 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router
|
||||
}
|
||||
|
||||
if (user.host !== null) {
|
||||
menu = menu.concat([null, {
|
||||
menu = menu.concat([{ type: 'divider' }, {
|
||||
icon: 'ti ti-refresh',
|
||||
text: i18n.ts.updateRemoteUser,
|
||||
action: userInfoUpdate,
|
||||
}]);
|
||||
}
|
||||
|
||||
|
||||
if (defaultStore.state.devMode) {
|
||||
menu = menu.concat([null, {
|
||||
menu = menu.concat([{ type: 'divider' }, {
|
||||
icon: 'ti ti-id',
|
||||
text: i18n.ts.copyUserId,
|
||||
action: () => {
|
||||
@@ -355,7 +355,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router
|
||||
}
|
||||
|
||||
if ($i && meId === user.id) {
|
||||
menu = menu.concat([null, {
|
||||
menu = menu.concat([{ type: 'divider' }, {
|
||||
icon: 'ti ti-pencil',
|
||||
text: i18n.ts.editProfile,
|
||||
action: () => {
|
||||
@@ -365,7 +365,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router
|
||||
}
|
||||
|
||||
if (userActions.length > 0) {
|
||||
menu = menu.concat([null, ...userActions.map(action => ({
|
||||
menu = menu.concat([{ type: 'divider' }, ...userActions.map(action => ({
|
||||
icon: 'ti ti-plug',
|
||||
text: action.title,
|
||||
action: () => {
|
||||
|
@@ -5,6 +5,7 @@
|
||||
|
||||
import { defineAsyncComponent, Ref, ref } from 'vue';
|
||||
import { popup } from '@/os.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
|
||||
class ReactionPicker {
|
||||
private src: Ref<HTMLElement | null> = ref(null);
|
||||
@@ -17,25 +18,27 @@ class ReactionPicker {
|
||||
}
|
||||
|
||||
public async init() {
|
||||
const reactionsRef = defaultStore.reactiveState.reactions;
|
||||
await popup(defineAsyncComponent(() => import('@/components/MkEmojiPickerDialog.vue')), {
|
||||
src: this.src,
|
||||
pinnedEmojis: reactionsRef,
|
||||
asReactionPicker: true,
|
||||
manualShowing: this.manualShowing,
|
||||
}, {
|
||||
done: reaction => {
|
||||
this.onChosen!(reaction);
|
||||
if (this.onChosen) this.onChosen(reaction);
|
||||
},
|
||||
close: () => {
|
||||
this.manualShowing.value = false;
|
||||
},
|
||||
closed: () => {
|
||||
this.src.value = null;
|
||||
this.onClosed!();
|
||||
if (this.onClosed) this.onClosed();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public show(src: HTMLElement, onChosen: ReactionPicker['onChosen'], onClosed: ReactionPicker['onClosed']) {
|
||||
public show(src: HTMLElement, onChosen?: ReactionPicker['onChosen'], onClosed?: ReactionPicker['onClosed']) {
|
||||
this.src.value = src;
|
||||
this.manualShowing.value = true;
|
||||
this.onChosen = onChosen;
|
||||
|
479
packages/frontend/src/scripts/snowfall-effect.ts
Normal file
479
packages/frontend/src/scripts/snowfall-effect.ts
Normal file
File diff suppressed because one or more lines are too long
@@ -119,6 +119,10 @@ export const defaultStore = markRaw(new Storage('base', {
|
||||
where: 'account',
|
||||
default: ['👍', '❤️', '😆', '🤔', '😮', '🎉', '💢', '😥', '😇', '🍮'],
|
||||
},
|
||||
pinnedEmojis: {
|
||||
where: 'account',
|
||||
default: [],
|
||||
},
|
||||
reactionAcceptance: {
|
||||
where: 'account',
|
||||
default: 'nonSensitiveOnly' as 'likeOnly' | 'likeOnlyForRemote' | 'nonSensitiveOnly' | 'nonSensitiveOnlyForLocalLikeOnlyForRemote' | null,
|
||||
@@ -271,19 +275,19 @@ export const defaultStore = markRaw(new Storage('base', {
|
||||
where: 'device',
|
||||
default: 'remote' as 'none' | 'remote' | 'always',
|
||||
},
|
||||
reactionPickerSize: {
|
||||
emojiPickerScale: {
|
||||
where: 'device',
|
||||
default: 1,
|
||||
},
|
||||
reactionPickerWidth: {
|
||||
emojiPickerWidth: {
|
||||
where: 'device',
|
||||
default: 1,
|
||||
},
|
||||
reactionPickerHeight: {
|
||||
emojiPickerHeight: {
|
||||
where: 'device',
|
||||
default: 2,
|
||||
},
|
||||
reactionPickerUseDrawerForMobile: {
|
||||
emojiPickerUseDrawerForMobile: {
|
||||
where: 'device',
|
||||
default: true,
|
||||
},
|
||||
@@ -412,6 +416,10 @@ export const defaultStore = markRaw(new Storage('base', {
|
||||
code: false,
|
||||
} as Record<string, boolean>,
|
||||
},
|
||||
enableSeasonalScreenEffect: {
|
||||
where: 'device',
|
||||
default: false,
|
||||
},
|
||||
|
||||
sound_masterVolume: {
|
||||
where: 'device',
|
||||
|
@@ -8,7 +8,7 @@ import { Ref } from 'vue';
|
||||
|
||||
export type MenuAction = (ev: MouseEvent) => void;
|
||||
|
||||
export type MenuDivider = null;
|
||||
export type MenuDivider = { type: 'divider' };
|
||||
export type MenuNull = undefined;
|
||||
export type MenuLabel = { type: 'label', text: string };
|
||||
export type MenuLink = { type: 'link', to: string, text: string, icon?: string, indicate?: boolean, avatar?: Misskey.entities.User };
|
||||
|
@@ -64,7 +64,7 @@ export function openInstanceMenu(ev: MouseEvent) {
|
||||
text: i18n.ts.charts,
|
||||
icon: 'ti ti-chart-line',
|
||||
to: '/about#charts',
|
||||
}, null, {
|
||||
}, { type: 'divider' }, {
|
||||
type: 'link',
|
||||
text: i18n.ts.ads,
|
||||
icon: 'ti ti-ad',
|
||||
@@ -79,7 +79,7 @@ export function openInstanceMenu(ev: MouseEvent) {
|
||||
text: i18n.ts.tools,
|
||||
icon: 'ti ti-tool',
|
||||
children: toolsMenuItems(),
|
||||
}, null, (instance.impressumUrl) ? {
|
||||
}, { type: 'divider' }, (instance.impressumUrl) ? {
|
||||
text: i18n.ts.impressum,
|
||||
icon: 'ti ti-file-invoice',
|
||||
action: () => {
|
||||
@@ -97,7 +97,7 @@ export function openInstanceMenu(ev: MouseEvent) {
|
||||
action: () => {
|
||||
window.open(instance.privacyPolicyUrl, '_blank', 'noopener');
|
||||
},
|
||||
} : undefined, (!instance.impressumUrl && !instance.tosUrl && !instance.privacyPolicyUrl) ? undefined : null, {
|
||||
} : undefined, (!instance.impressumUrl && !instance.tosUrl && !instance.privacyPolicyUrl) ? undefined : { type: 'divider' }, {
|
||||
text: i18n.ts.help,
|
||||
icon: 'ti ti-help-circle',
|
||||
action: () => {
|
||||
|
@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<XSidebar v-if="!isMobile"/>
|
||||
|
||||
<div :class="$style.main">
|
||||
<XAnnouncements v-if="$i" :class="$style.announcements"/>
|
||||
<XAnnouncements v-if="$i"/>
|
||||
<XStatusBars/>
|
||||
<div ref="columnsEl" :class="[$style.sections, { [$style.center]: deckStore.reactiveState.columnAlign.value === 'center', [$style.snapScroll]: snapScroll }]" @contextmenu.self.prevent="onContextmenu" @wheel.self="onWheel">
|
||||
<!-- sectionを利用しているのは、deck.vue側でcolumnに対してfirst-of-typeを効かせるため -->
|
||||
@@ -236,7 +236,7 @@ function changeProfile(ev: MouseEvent) {
|
||||
deckStore.set('profile', k);
|
||||
unisonReload();
|
||||
},
|
||||
}))), null, {
|
||||
}))), { type: 'divider' }, {
|
||||
text: i18n.ts._deck.newProfile,
|
||||
icon: 'ti ti-plus',
|
||||
action: async () => {
|
||||
|
@@ -104,7 +104,7 @@ function toggleActive() {
|
||||
}
|
||||
|
||||
function getMenu() {
|
||||
let items = [{
|
||||
let items: MenuItem[] = [{
|
||||
icon: 'ti ti-settings',
|
||||
text: i18n.ts._deck.configureColumn,
|
||||
action: async () => {
|
||||
@@ -170,7 +170,7 @@ function getMenu() {
|
||||
action: () => {
|
||||
popRightColumn(props.column.id);
|
||||
},
|
||||
} : undefined, null, {
|
||||
} : undefined, { type: 'divider' }, {
|
||||
icon: 'ti ti-trash',
|
||||
text: i18n.ts.remove,
|
||||
danger: true,
|
||||
@@ -180,7 +180,7 @@ function getMenu() {
|
||||
}];
|
||||
|
||||
if (props.menu) {
|
||||
items.unshift(null);
|
||||
items.unshift({ type: 'divider' });
|
||||
items = props.menu.concat(items);
|
||||
}
|
||||
|
||||
|
@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkStickyContainer ref="contents" :class="$style.contents" style="container-type: inline-size;" @contextmenu.stop="onContextmenu">
|
||||
<template #header>
|
||||
<div>
|
||||
<XAnnouncements v-if="$i" :class="$style.announcements"/>
|
||||
<XAnnouncements v-if="$i"/>
|
||||
<XStatusBars :class="$style.statusbars"/>
|
||||
</div>
|
||||
</template>
|
||||
|
@@ -72,5 +72,6 @@ defineExpose<WidgetComponentExpose>({
|
||||
height: 350px;
|
||||
border: none;
|
||||
pointer-events: none;
|
||||
color-scheme: light;
|
||||
}
|
||||
</style>
|
||||
|
@@ -134,7 +134,7 @@ const choose = async (ev) => {
|
||||
text: i18n.ts._timelines.global,
|
||||
icon: 'ti ti-whirl',
|
||||
action: () => { setSrc('global'); },
|
||||
}, antennaItems.length > 0 ? null : undefined, ...antennaItems, listItems.length > 0 ? null : undefined, ...listItems], ev.currentTarget ?? ev.target).then(() => {
|
||||
}, antennaItems.length > 0 ? { type: 'divider' } : undefined, ...antennaItems, listItems.length > 0 ? { type: 'divider' } : undefined, ...listItems], ev.currentTarget ?? ev.target).then(() => {
|
||||
menuOpened.value = false;
|
||||
});
|
||||
};
|
||||
|
@@ -3,13 +3,14 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { describe, test, assert, afterEach } from 'vitest';
|
||||
import { render, cleanup, type RenderResult } from '@testing-library/vue';
|
||||
import { afterEach, assert, describe, test } from 'vitest';
|
||||
import { cleanup, render, type RenderResult } from '@testing-library/vue';
|
||||
import './init';
|
||||
import type * as Misskey from 'misskey-js';
|
||||
import { directives } from '@/directives/index.js';
|
||||
import { components } from '@/components/index.js';
|
||||
import XHome from '@/pages/user/home.vue';
|
||||
import 'intersection-observer';
|
||||
|
||||
describe('XHome', () => {
|
||||
const renderHome = (user: Partial<Misskey.entities.UserDetailed>): RenderResult => {
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import dns from 'dns';
|
||||
import { defineConfig } from 'vite';
|
||||
import locales from '../../locales';
|
||||
import { getConfig } from './vite.config.js';
|
||||
|
||||
dns.setDefaultResultOrder('ipv4first');
|
||||
@@ -49,6 +50,11 @@ const devConfig = {
|
||||
input: 'index.html',
|
||||
},
|
||||
},
|
||||
|
||||
define: {
|
||||
...defaultConfig.define,
|
||||
_LANGS_FULL_: JSON.stringify(Object.entries(locales)),
|
||||
},
|
||||
};
|
||||
|
||||
export default defineConfig(({ command, mode }) => devConfig);
|
||||
|
Reference in New Issue
Block a user