-
-
-
-
-
-
-
-
{{ i18n.ts.sensitive }}
-
{{ i18n.ts.clickToShow }}
-
-
-
-
-
-
-
-
+
+
-
{{ i18n.ts.nothing }}
+
{{ i18n.ts.nothing }}
@@ -35,45 +20,34 @@ SPDX-License-Identifier: AGPL-3.0-only
diff --git a/packages/frontend/src/components/MkRoleSelectDialog.stories.impl.ts b/packages/frontend/src/components/MkRoleSelectDialog.stories.impl.ts
new file mode 100644
index 0000000000..411d62edf9
--- /dev/null
+++ b/packages/frontend/src/components/MkRoleSelectDialog.stories.impl.ts
@@ -0,0 +1,106 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+/* eslint-disable @typescript-eslint/explicit-function-return-type */
+import { StoryObj } from '@storybook/vue3';
+import { http, HttpResponse } from 'msw';
+import { role } from '../../.storybook/fakes.js';
+import { commonHandlers } from '../../.storybook/mocks.js';
+import MkRoleSelectDialog from '@/components/MkRoleSelectDialog.vue';
+
+const roles = [
+ role({ displayOrder: 1 }, '1'), role({ displayOrder: 1 }, '1'), role({ displayOrder: 1 }, '1'), role({ displayOrder: 1 }, '1'),
+ role({ displayOrder: 2 }, '2'), role({ displayOrder: 2 }, '2'), role({ displayOrder: 3 }, '3'), role({ displayOrder: 3 }, '3'),
+ role({ displayOrder: 4 }, '4'), role({ displayOrder: 5 }, '5'), role({ displayOrder: 6 }, '6'), role({ displayOrder: 7 }, '7'),
+ role({ displayOrder: 999, name: 'privateRole', isPublic: false }, '999'),
+];
+
+export const Default = {
+ render(args) {
+ return {
+ components: {
+ MkRoleSelectDialog,
+ },
+ setup() {
+ return {
+ args,
+ };
+ },
+ computed: {
+ props() {
+ return {
+ ...this.args,
+ };
+ },
+ },
+ template: '
',
+ };
+ },
+ args: {
+ initialRoleIds: undefined,
+ infoMessage: undefined,
+ title: undefined,
+ publicOnly: true,
+ },
+ parameters: {
+ layout: 'centered',
+ msw: {
+ handlers: [
+ ...commonHandlers,
+ http.post('/api/admin/roles/list', ({ params }) => {
+ return HttpResponse.json(roles);
+ }),
+ ],
+ },
+ },
+ decorators: [() => ({
+ template: '
',
+ })],
+} satisfies StoryObj
;
+
+export const InitialIds = {
+ ...Default,
+ args: {
+ ...Default.args,
+ initialRoleIds: [roles[0].id, roles[1].id, roles[4].id, roles[6].id, roles[8].id, roles[10].id],
+ },
+} satisfies StoryObj;
+
+export const InfoMessage = {
+ ...Default,
+ args: {
+ ...Default.args,
+ infoMessage: 'This is a message.',
+ },
+} satisfies StoryObj;
+
+export const Title = {
+ ...Default,
+ args: {
+ ...Default.args,
+ title: 'Select roles',
+ },
+} satisfies StoryObj;
+
+export const Full = {
+ ...Default,
+ args: {
+ ...Default.args,
+ initialRoleIds: roles.map(it => it.id),
+ infoMessage: InfoMessage.args.infoMessage,
+ title: Title.args.title,
+ },
+} satisfies StoryObj;
+
+export const FullWithPrivate = {
+ ...Default,
+ args: {
+ ...Default.args,
+ initialRoleIds: roles.map(it => it.id),
+ infoMessage: InfoMessage.args.infoMessage,
+ title: Title.args.title,
+ publicOnly: false,
+ },
+} satisfies StoryObj;
diff --git a/packages/frontend/src/components/MkRoleSelectDialog.vue b/packages/frontend/src/components/MkRoleSelectDialog.vue
new file mode 100644
index 0000000000..67a7a3f752
--- /dev/null
+++ b/packages/frontend/src/components/MkRoleSelectDialog.vue
@@ -0,0 +1,200 @@
+
+
+
+
+ {{ title }}
+
+
+
+
+ {{ i18n.ts.add }}
+
+
+
+
+ {{ i18n.ts._roleSelectDialog.notSelected }}
+
+
+
{{ infoMessage }}
+
+
+ {{ i18n.ts.ok }}
+ {{ i18n.ts.cancel }}
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend/src/components/MkSortOrderEditor.define.ts b/packages/frontend/src/components/MkSortOrderEditor.define.ts
new file mode 100644
index 0000000000..f023b5d72b
--- /dev/null
+++ b/packages/frontend/src/components/MkSortOrderEditor.define.ts
@@ -0,0 +1,11 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export type SortOrderDirection = '+' | '-'
+
+export type SortOrder = {
+ key: T;
+ direction: SortOrderDirection;
+}
diff --git a/packages/frontend/src/components/MkSortOrderEditor.vue b/packages/frontend/src/components/MkSortOrderEditor.vue
new file mode 100644
index 0000000000..da08f12297
--- /dev/null
+++ b/packages/frontend/src/components/MkSortOrderEditor.vue
@@ -0,0 +1,112 @@
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend/src/components/MkTagItem.stories.impl.ts b/packages/frontend/src/components/MkTagItem.stories.impl.ts
new file mode 100644
index 0000000000..3f243ff651
--- /dev/null
+++ b/packages/frontend/src/components/MkTagItem.stories.impl.ts
@@ -0,0 +1,70 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+/* eslint-disable @typescript-eslint/explicit-function-return-type */
+/* eslint-disable import/no-default-export */
+import { action } from '@storybook/addon-actions';
+import { StoryObj } from '@storybook/vue3';
+import MkTagItem from './MkTagItem.vue';
+
+export const Default = {
+ render(args) {
+ return {
+ components: {
+ MkTagItem: MkTagItem,
+ },
+ setup() {
+ return {
+ args,
+ };
+ },
+ computed: {
+ props() {
+ return {
+ ...this.args,
+ };
+ },
+ events() {
+ return {
+ click: action('click'),
+ exButtonClick: action('exButtonClick'),
+ };
+ },
+ },
+ template: ' ',
+ };
+ },
+ args: {
+ content: 'name',
+ },
+ parameters: {
+ layout: 'centered',
+ },
+} satisfies StoryObj;
+
+export const Icon = {
+ ...Default,
+ args: {
+ ...Default.args,
+ iconClass: 'ti ti-arrow-up',
+ },
+} satisfies StoryObj;
+
+export const ExButton = {
+ ...Default,
+ args: {
+ ...Default.args,
+ exButtonIconClass: 'ti ti-x',
+ },
+} satisfies StoryObj;
+
+export const IconExButton = {
+ ...Default,
+ args: {
+ ...Default.args,
+ iconClass: 'ti ti-arrow-up',
+ exButtonIconClass: 'ti ti-x',
+ },
+} satisfies StoryObj;
diff --git a/packages/frontend/src/components/MkTagItem.vue b/packages/frontend/src/components/MkTagItem.vue
new file mode 100644
index 0000000000..98f2411392
--- /dev/null
+++ b/packages/frontend/src/components/MkTagItem.vue
@@ -0,0 +1,76 @@
+
+
+
+ emit('click', ev)">
+
+ {{ content }}
+ emit('exButtonClick', ev)">
+
+
+
+
+
+
+
+
diff --git a/packages/frontend/src/components/grid/MkCellTooltip.vue b/packages/frontend/src/components/grid/MkCellTooltip.vue
new file mode 100644
index 0000000000..fd289c6cd9
--- /dev/null
+++ b/packages/frontend/src/components/grid/MkCellTooltip.vue
@@ -0,0 +1,35 @@
+
+
+
+
+
+ {{ content }}
+
+
+
+
+
+
+
diff --git a/packages/frontend/src/components/grid/MkDataCell.vue b/packages/frontend/src/components/grid/MkDataCell.vue
new file mode 100644
index 0000000000..0ffd42abda
--- /dev/null
+++ b/packages/frontend/src/components/grid/MkDataCell.vue
@@ -0,0 +1,391 @@
+
+
+
+
+
+
+
+
+ {{ cell.value }}
+
+
+ {{ cell.value }}
+
+
+ {{ cell.value }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend/src/components/grid/MkDataRow.vue b/packages/frontend/src/components/grid/MkDataRow.vue
new file mode 100644
index 0000000000..280a14bc4a
--- /dev/null
+++ b/packages/frontend/src/components/grid/MkDataRow.vue
@@ -0,0 +1,72 @@
+
+
+
+
+
+ emit('operation:beginEdit', sender)"
+ @operation:endEdit="(sender) => emit('operation:endEdit', sender)"
+ @change:value="(sender, newValue) => emit('change:value', sender, newValue)"
+ @change:contentSize="(sender, newSize) => emit('change:contentSize', sender, newSize)"
+ />
+
+
+
+
+
+
diff --git a/packages/frontend/src/components/grid/MkGrid.stories.impl.ts b/packages/frontend/src/components/grid/MkGrid.stories.impl.ts
new file mode 100644
index 0000000000..5801012f15
--- /dev/null
+++ b/packages/frontend/src/components/grid/MkGrid.stories.impl.ts
@@ -0,0 +1,223 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+/* eslint-disable @typescript-eslint/explicit-function-return-type */
+import { action } from '@storybook/addon-actions';
+import { StoryObj } from '@storybook/vue3';
+import { ref } from 'vue';
+import { commonHandlers } from '../../../.storybook/mocks.js';
+import { boolean, choose, country, date, firstName, integer, lastName, text } from '../../../.storybook/fake-utils.js';
+import MkGrid from './MkGrid.vue';
+import { GridContext, GridEvent } from '@/components/grid/grid-event.js';
+import { DataSource, GridSetting } from '@/components/grid/grid.js';
+import { GridColumnSetting } from '@/components/grid/column.js';
+
+function d(p: {
+ check?: boolean,
+ name?: string,
+ email?: string,
+ age?: number,
+ birthday?: string,
+ gender?: string,
+ country?: string,
+ reportCount?: number,
+ createdAt?: string,
+}, seed: string) {
+ const prefix = text(10, seed);
+
+ return {
+ check: p.check ?? boolean(seed),
+ name: p.name ?? `${firstName(seed)} ${lastName(seed)}`,
+ email: p.email ?? `${prefix}@example.com`,
+ age: p.age ?? integer(20, 80, seed),
+ birthday: date({}, seed).toISOString(),
+ gender: p.gender ?? choose(['male', 'female', 'other', 'unknown'], seed),
+ country: p.country ?? country(seed),
+ reportCount: p.reportCount ?? integer(0, 9999, seed),
+ createdAt: p.createdAt ?? date({}, seed).toISOString(),
+ };
+}
+
+const defaultCols: GridColumnSetting[] = [
+ { bindTo: 'check', icon: 'ti-check', type: 'boolean', width: 50 },
+ { bindTo: 'name', title: 'Name', type: 'text', width: 'auto' },
+ { bindTo: 'email', title: 'Email', type: 'text', width: 'auto' },
+ { bindTo: 'age', title: 'Age', type: 'number', width: 50 },
+ { bindTo: 'birthday', title: 'Birthday', type: 'date', width: 'auto' },
+ { bindTo: 'gender', title: 'Gender', type: 'text', width: 80 },
+ { bindTo: 'country', title: 'Country', type: 'text', width: 120 },
+ { bindTo: 'reportCount', title: 'ReportCount', type: 'number', width: 'auto' },
+ { bindTo: 'createdAt', title: 'CreatedAt', type: 'date', width: 'auto' },
+];
+
+function createArgs(overrides?: { settings?: Partial, data?: DataSource[] }) {
+ const refData = ref[]>([]);
+ for (let i = 0; i < 100; i++) {
+ refData.value.push(d({}, i.toString()));
+ }
+
+ return {
+ settings: {
+ row: overrides?.settings?.row,
+ cols: [
+ ...defaultCols.filter(col => overrides?.settings?.cols?.every(c => c.bindTo !== col.bindTo) ?? true),
+ ...overrides?.settings?.cols ?? [],
+ ],
+ cells: overrides?.settings?.cells,
+ },
+ data: refData.value,
+ };
+}
+
+function createRender(params: { settings: GridSetting, data: DataSource[] }) {
+ return {
+ render(args) {
+ return {
+ components: {
+ MkGrid,
+ },
+ setup() {
+ return {
+ args,
+ };
+ },
+ data() {
+ return {
+ data: args.data,
+ };
+ },
+ computed: {
+ props() {
+ return {
+ ...args,
+ };
+ },
+ events() {
+ return {
+ event: (event: GridEvent, context: GridContext) => {
+ switch (event.type) {
+ case 'cell-value-change': {
+ args.data[event.row.index][event.column.setting.bindTo] = event.newValue;
+ }
+ }
+ },
+ };
+ },
+ },
+ template: '
',
+ };
+ },
+ args: {
+ ...params,
+ },
+ parameters: {
+ layout: 'fullscreen',
+ msw: {
+ handlers: [
+ ...commonHandlers,
+ ],
+ },
+ },
+ } satisfies StoryObj;
+}
+
+export const Default = createRender(createArgs());
+
+export const NoNumber = createRender(createArgs({
+ settings: {
+ row: {
+ showNumber: false,
+ },
+ },
+}));
+
+export const NoSelectable = createRender(createArgs({
+ settings: {
+ row: {
+ selectable: false,
+ },
+ },
+}));
+
+export const Editable = createRender(createArgs({
+ settings: {
+ cols: defaultCols.map(col => ({ ...col, editable: true })),
+ },
+}));
+
+export const AdditionalRowStyle = createRender(createArgs({
+ settings: {
+ cols: defaultCols.map(col => ({ ...col, editable: true })),
+ row: {
+ styleRules: [
+ {
+ condition: ({ row }) => AdditionalRowStyle.args.data[row.index].check as boolean,
+ applyStyle: {
+ style: {
+ backgroundColor: 'lightgray',
+ },
+ },
+ },
+ ],
+ },
+ },
+}));
+
+export const ContextMenu = createRender(createArgs({
+ settings: {
+ cols: [
+ {
+ bindTo: 'check', icon: 'ti-check', type: 'boolean', width: 50, contextMenuFactory: (col, context) => [
+ {
+ type: 'button',
+ text: 'Check All',
+ action: () => {
+ for (const d of ContextMenu.args.data) {
+ d.check = true;
+ }
+ },
+ },
+ {
+ type: 'button',
+ text: 'Uncheck All',
+ action: () => {
+ for (const d of ContextMenu.args.data) {
+ d.check = false;
+ }
+ },
+ },
+ ],
+ },
+ ],
+ row: {
+ contextMenuFactory: (row, context) => [
+ {
+ type: 'button',
+ text: 'Delete',
+ action: () => {
+ const idxes = context.rangedRows.map(r => r.index);
+ const newData = ContextMenu.args.data.filter((d, i) => !idxes.includes(i));
+
+ ContextMenu.args.data.splice(0);
+ ContextMenu.args.data.push(...newData);
+ },
+ },
+ ],
+ },
+ cells: {
+ contextMenuFactory: (col, row, value, context) => [
+ {
+ type: 'button',
+ text: 'Delete',
+ action: () => {
+ for (const cell of context.rangedCells) {
+ ContextMenu.args.data[cell.row.index][cell.column.setting.bindTo] = undefined;
+ }
+ },
+ },
+ ],
+ },
+ },
+}));
diff --git a/packages/frontend/src/components/grid/MkGrid.vue b/packages/frontend/src/components/grid/MkGrid.vue
new file mode 100644
index 0000000000..60738365fb
--- /dev/null
+++ b/packages/frontend/src/components/grid/MkGrid.vue
@@ -0,0 +1,1342 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend/src/components/grid/MkHeaderCell.vue b/packages/frontend/src/components/grid/MkHeaderCell.vue
new file mode 100644
index 0000000000..605d27c6d6
--- /dev/null
+++ b/packages/frontend/src/components/grid/MkHeaderCell.vue
@@ -0,0 +1,216 @@
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend/src/components/grid/MkHeaderRow.vue b/packages/frontend/src/components/grid/MkHeaderRow.vue
new file mode 100644
index 0000000000..8affa08fd5
--- /dev/null
+++ b/packages/frontend/src/components/grid/MkHeaderRow.vue
@@ -0,0 +1,60 @@
+
+
+
+
+
+ emit('operation:beginWidthChange', sender)"
+ @operation:endWidthChange="(sender) => emit('operation:endWidthChange', sender)"
+ @operation:widthLargest="(sender) => emit('operation:widthLargest', sender)"
+ @change:width="(sender, width) => emit('change:width', sender, width)"
+ @change:contentSize="(sender, newSize) => emit('change:contentSize', sender, newSize)"
+ />
+
+
+
+
+
+
diff --git a/packages/frontend/src/components/grid/MkNumberCell.vue b/packages/frontend/src/components/grid/MkNumberCell.vue
new file mode 100644
index 0000000000..674bba96bc
--- /dev/null
+++ b/packages/frontend/src/components/grid/MkNumberCell.vue
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend/src/components/grid/cell-validators.ts b/packages/frontend/src/components/grid/cell-validators.ts
new file mode 100644
index 0000000000..949cab2ec6
--- /dev/null
+++ b/packages/frontend/src/components/grid/cell-validators.ts
@@ -0,0 +1,110 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { CellValue, GridCell } from '@/components/grid/cell.js';
+import { GridColumn } from '@/components/grid/column.js';
+import { GridRow } from '@/components/grid/row.js';
+import { i18n } from '@/i18n.js';
+
+export type ValidatorParams = {
+ column: GridColumn;
+ row: GridRow;
+ value: CellValue;
+ allCells: GridCell[];
+};
+
+export type ValidatorResult = {
+ valid: boolean;
+ message?: string;
+}
+
+export type GridCellValidator = {
+ name?: string;
+ ignoreViolation?: boolean;
+ validate: (params: ValidatorParams) => ValidatorResult;
+}
+
+export type ValidateViolation = {
+ valid: boolean;
+ params: ValidatorParams;
+ violations: ValidateViolationItem[];
+}
+
+export type ValidateViolationItem = {
+ valid: boolean;
+ validator: GridCellValidator;
+ result: ValidatorResult;
+}
+
+export function cellValidation(allCells: GridCell[], cell: GridCell, newValue: CellValue): ValidateViolation {
+ const { column, row } = cell;
+ const validators = column.setting.validators ?? [];
+
+ const params: ValidatorParams = {
+ column,
+ row,
+ value: newValue,
+ allCells,
+ };
+
+ const violations: ValidateViolationItem[] = validators.map(validator => {
+ const result = validator.validate(params);
+ return {
+ valid: result.valid,
+ validator,
+ result,
+ };
+ });
+
+ return {
+ valid: violations.every(v => v.result.valid),
+ params,
+ violations,
+ };
+}
+
+class ValidatorPreset {
+ required(): GridCellValidator {
+ return {
+ name: 'required',
+ validate: ({ value }): ValidatorResult => {
+ return {
+ valid: value !== null && value !== undefined && value !== '',
+ message: i18n.ts._gridComponent._error.requiredValue,
+ };
+ },
+ };
+ }
+
+ regex(pattern: RegExp): GridCellValidator {
+ return {
+ name: 'regex',
+ validate: ({ value }): ValidatorResult => {
+ return {
+ valid: (typeof value !== 'string') || pattern.test(value.toString() ?? ''),
+ message: i18n.tsx._gridComponent._error.patternNotMatch({ pattern: pattern.source }),
+ };
+ },
+ };
+ }
+
+ unique(): GridCellValidator {
+ return {
+ name: 'unique',
+ validate: ({ column, row, value, allCells }): ValidatorResult => {
+ const bindTo = column.setting.bindTo;
+ const isUnique = allCells
+ .filter(it => it.column.setting.bindTo === bindTo && it.row.index !== row.index)
+ .every(cell => cell.value !== value);
+ return {
+ valid: isUnique,
+ message: i18n.ts._gridComponent._error.notUnique,
+ };
+ },
+ };
+ }
+}
+
+export const validators = new ValidatorPreset();
diff --git a/packages/frontend/src/components/grid/cell.ts b/packages/frontend/src/components/grid/cell.ts
new file mode 100644
index 0000000000..71b7a3e3f1
--- /dev/null
+++ b/packages/frontend/src/components/grid/cell.ts
@@ -0,0 +1,88 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { ValidateViolation } from '@/components/grid/cell-validators.js';
+import { Size } from '@/components/grid/grid.js';
+import { GridColumn } from '@/components/grid/column.js';
+import { GridRow } from '@/components/grid/row.js';
+import { MenuItem } from '@/types/menu.js';
+import { GridContext } from '@/components/grid/grid-event.js';
+
+export type CellValue = string | boolean | number | undefined | null | Array | NonNullable;
+
+export type CellAddress = {
+ row: number;
+ col: number;
+}
+
+export const CELL_ADDRESS_NONE: CellAddress = {
+ row: -1,
+ col: -1,
+};
+
+export type GridCell = {
+ address: CellAddress;
+ value: CellValue;
+ column: GridColumn;
+ row: GridRow;
+ selected: boolean;
+ ranged: boolean;
+ contentSize: Size;
+ setting: GridCellSetting;
+ violation: ValidateViolation;
+}
+
+export type GridCellContextMenuFactory = (col: GridColumn, row: GridRow, value: CellValue, context: GridContext) => MenuItem[];
+
+export type GridCellSetting = {
+ contextMenuFactory?: GridCellContextMenuFactory;
+}
+
+export function createCell(
+ column: GridColumn,
+ row: GridRow,
+ value: CellValue,
+ setting: GridCellSetting,
+): GridCell {
+ const newValue = (row.using && column.setting.valueTransformer)
+ ? column.setting.valueTransformer(row, column, value)
+ : value;
+
+ return {
+ address: { row: row.index, col: column.index },
+ value: newValue,
+ column,
+ row,
+ selected: false,
+ ranged: false,
+ contentSize: { width: 0, height: 0 },
+ violation: {
+ valid: true,
+ params: {
+ column,
+ row,
+ value,
+ allCells: [],
+ },
+ violations: [],
+ },
+ setting,
+ };
+}
+
+export function resetCell(cell: GridCell): void {
+ cell.selected = false;
+ cell.ranged = false;
+ cell.violation = {
+ valid: true,
+ params: {
+ column: cell.column,
+ row: cell.row,
+ value: cell.value,
+ allCells: [],
+ },
+ violations: [],
+ };
+}
diff --git a/packages/frontend/src/components/grid/column.ts b/packages/frontend/src/components/grid/column.ts
new file mode 100644
index 0000000000..2f505756fe
--- /dev/null
+++ b/packages/frontend/src/components/grid/column.ts
@@ -0,0 +1,53 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { GridCellValidator } from '@/components/grid/cell-validators.js';
+import { Size, SizeStyle } from '@/components/grid/grid.js';
+import { calcCellWidth } from '@/components/grid/grid-utils.js';
+import { CellValue, GridCell } from '@/components/grid/cell.js';
+import { GridRow } from '@/components/grid/row.js';
+import { MenuItem } from '@/types/menu.js';
+import { GridContext } from '@/components/grid/grid-event.js';
+
+export type ColumnType = 'text' | 'number' | 'date' | 'boolean' | 'image' | 'hidden';
+
+export type CustomValueEditor = (row: GridRow, col: GridColumn, value: CellValue, cellElement: HTMLElement) => Promise;
+export type CellValueTransformer = (row: GridRow, col: GridColumn, value: CellValue) => CellValue;
+export type GridColumnContextMenuFactory = (col: GridColumn, context: GridContext) => MenuItem[];
+
+export type GridColumnSetting = {
+ bindTo: string;
+ title?: string;
+ icon?: string;
+ type: ColumnType;
+ width: SizeStyle;
+ editable?: boolean;
+ validators?: GridCellValidator[];
+ customValueEditor?: CustomValueEditor;
+ valueTransformer?: CellValueTransformer;
+ contextMenuFactory?: GridColumnContextMenuFactory;
+ events?: {
+ copy?: (value: CellValue) => string;
+ paste?: (text: string) => CellValue;
+ delete?: (cell: GridCell, context: GridContext) => void;
+ }
+};
+
+export type GridColumn = {
+ index: number;
+ setting: GridColumnSetting;
+ width: string;
+ contentSize: Size;
+}
+
+export function createColumn(setting: GridColumnSetting, index: number): GridColumn {
+ return {
+ index,
+ setting,
+ width: calcCellWidth(setting.width),
+ contentSize: { width: 0, height: 0 },
+ };
+}
+
diff --git a/packages/frontend/src/components/grid/grid-event.ts b/packages/frontend/src/components/grid/grid-event.ts
new file mode 100644
index 0000000000..074b72b956
--- /dev/null
+++ b/packages/frontend/src/components/grid/grid-event.ts
@@ -0,0 +1,46 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { CellAddress, CellValue, GridCell } from '@/components/grid/cell.js';
+import { GridState } from '@/components/grid/grid.js';
+import { ValidateViolation } from '@/components/grid/cell-validators.js';
+import { GridColumn } from '@/components/grid/column.js';
+import { GridRow } from '@/components/grid/row.js';
+
+export type GridContext = {
+ selectedCell?: GridCell;
+ rangedCells: GridCell[];
+ rangedRows: GridRow[];
+ randedBounds: {
+ leftTop: CellAddress;
+ rightBottom: CellAddress;
+ };
+ availableBounds: {
+ leftTop: CellAddress;
+ rightBottom: CellAddress;
+ };
+ state: GridState;
+ rows: GridRow[];
+ columns: GridColumn[];
+};
+
+export type GridEvent =
+ GridCellValueChangeEvent |
+ GridCellValidationEvent
+ ;
+
+export type GridCellValueChangeEvent = {
+ type: 'cell-value-change';
+ column: GridColumn;
+ row: GridRow;
+ oldValue: CellValue;
+ newValue: CellValue;
+};
+
+export type GridCellValidationEvent = {
+ type: 'cell-validation';
+ violation?: ValidateViolation;
+ all: ValidateViolation[];
+};
diff --git a/packages/frontend/src/components/grid/grid-utils.ts b/packages/frontend/src/components/grid/grid-utils.ts
new file mode 100644
index 0000000000..a45bc88926
--- /dev/null
+++ b/packages/frontend/src/components/grid/grid-utils.ts
@@ -0,0 +1,215 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { isRef, Ref } from 'vue';
+import { DataSource, SizeStyle } from '@/components/grid/grid.js';
+import { CELL_ADDRESS_NONE, CellAddress, CellValue, GridCell } from '@/components/grid/cell.js';
+import { GridRow } from '@/components/grid/row.js';
+import { GridContext } from '@/components/grid/grid-event.js';
+import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
+import { GridColumn, GridColumnSetting } from '@/components/grid/column.js';
+
+export function isCellElement(elem: HTMLElement): boolean {
+ return elem.hasAttribute('data-grid-cell');
+}
+
+export function isRowElement(elem: HTMLElement): boolean {
+ return elem.hasAttribute('data-grid-row');
+}
+
+export function calcCellWidth(widthSetting: SizeStyle): string {
+ switch (widthSetting) {
+ case undefined:
+ case 'auto': {
+ return 'auto';
+ }
+ default: {
+ return `${widthSetting}px`;
+ }
+ }
+}
+
+function getCellRowByAttribute(elem: HTMLElement): number {
+ const row = elem.getAttribute('data-grid-cell-row');
+ if (row === null) {
+ throw new Error('data-grid-cell-row attribute not found');
+ }
+ return Number(row);
+}
+
+function getCellColByAttribute(elem: HTMLElement): number {
+ const col = elem.getAttribute('data-grid-cell-col');
+ if (col === null) {
+ throw new Error('data-grid-cell-col attribute not found');
+ }
+ return Number(col);
+}
+
+export function getCellAddress(elem: HTMLElement, parentNodeCount = 10): CellAddress {
+ let node = elem;
+ for (let i = 0; i < parentNodeCount; i++) {
+ if (!node.parentElement) {
+ break;
+ }
+
+ if (isCellElement(node) && isRowElement(node.parentElement)) {
+ const row = getCellRowByAttribute(node);
+ const col = getCellColByAttribute(node);
+
+ return { row, col };
+ }
+
+ node = node.parentElement;
+ }
+
+ return CELL_ADDRESS_NONE;
+}
+
+export function getCellElement(elem: HTMLElement, parentNodeCount = 10): HTMLElement | null {
+ let node = elem;
+ for (let i = 0; i < parentNodeCount; i++) {
+ if (isCellElement(node)) {
+ return node;
+ }
+
+ if (!node.parentElement) {
+ break;
+ }
+
+ node = node.parentElement;
+ }
+
+ return null;
+}
+
+export function equalCellAddress(a: CellAddress, b: CellAddress): boolean {
+ return a.row === b.row && a.col === b.col;
+}
+
+/**
+ * グリッドの選択範囲の内容をタブ区切り形式テキストに変換してクリップボードにコピーする。
+ */
+export function copyGridDataToClipboard(
+ gridItems: Ref | DataSource[],
+ context: GridContext,
+) {
+ const items = isRef(gridItems) ? gridItems.value : gridItems;
+ const lines = Array.of();
+ const bounds = context.randedBounds;
+
+ for (let row = bounds.leftTop.row; row <= bounds.rightBottom.row; row++) {
+ const rowItems = Array.of();
+ for (let col = bounds.leftTop.col; col <= bounds.rightBottom.col; col++) {
+ const { bindTo, events } = context.columns[col].setting;
+ const value = items[row][bindTo];
+ const transformValue = events?.copy
+ ? events.copy(value)
+ : typeof value === 'object' || Array.isArray(value)
+ ? JSON.stringify(value)
+ : value?.toString() ?? '';
+ rowItems.push(transformValue);
+ }
+ lines.push(rowItems.join('\t'));
+ }
+
+ const text = lines.join('\n');
+ copyToClipboard(text);
+
+ if (_DEV_) {
+ console.log(`Copied to clipboard: ${text}`);
+ }
+}
+
+/**
+ * クリップボードからタブ区切りテキストとして値を読み取り、グリッドの選択範囲に貼り付けるためのユーティリティ関数。
+ * …と言いつつも、使用箇所により反映方法に差があるため更新操作はコールバック関数に任せている。
+ */
+export async function pasteToGridFromClipboard(
+ context: GridContext,
+ callback: (row: GridRow, col: GridColumn, parsedValue: CellValue) => void,
+) {
+ function parseValue(value: string, setting: GridColumnSetting): CellValue {
+ if (setting.events?.paste) {
+ return setting.events.paste(value);
+ } else {
+ switch (setting.type) {
+ case 'number': {
+ return Number(value);
+ }
+ case 'boolean': {
+ return value === 'true';
+ }
+ default: {
+ return value;
+ }
+ }
+ }
+ }
+
+ const clipBoardText = await navigator.clipboard.readText();
+ if (_DEV_) {
+ console.log(`Paste from clipboard: ${clipBoardText}`);
+ }
+
+ const bounds = context.randedBounds;
+ const lines = clipBoardText.replace(/\r/g, '')
+ .split('\n')
+ .map(it => it.split('\t'));
+
+ if (lines.length === 1 && lines[0].length === 1) {
+ // 単独文字列の場合は選択範囲全体に同じテキストを貼り付ける
+ const ranges = context.rangedCells;
+ for (const cell of ranges) {
+ if (cell.column.setting.editable) {
+ callback(cell.row, cell.column, parseValue(lines[0][0], cell.column.setting));
+ }
+ }
+ } else {
+ // 表形式文字列の場合は表形式にパースし、選択範囲に合うように貼り付ける
+ const offsetRow = bounds.leftTop.row;
+ const offsetCol = bounds.leftTop.col;
+ const { columns, rows } = context;
+ for (let row = bounds.leftTop.row; row <= bounds.rightBottom.row; row++) {
+ const rowIdx = row - offsetRow;
+ if (lines.length <= rowIdx) {
+ // クリップボードから読んだ二次元配列よりも選択範囲の方が大きい場合、貼り付け操作を打ち切る
+ break;
+ }
+
+ const items = lines[rowIdx];
+ for (let col = bounds.leftTop.col; col <= bounds.rightBottom.col; col++) {
+ const colIdx = col - offsetCol;
+ if (items.length <= colIdx) {
+ // クリップボードから読んだ二次元配列よりも選択範囲の方が大きい場合、貼り付け操作を打ち切る
+ break;
+ }
+
+ if (columns[col].setting.editable) {
+ callback(rows[row], columns[col], parseValue(items[colIdx], columns[col].setting));
+ }
+ }
+ }
+ }
+}
+
+/**
+ * グリッドの選択範囲にあるデータを削除するためのユーティリティ関数。
+ * …と言いつつも、使用箇所により反映方法に差があるため更新操作はコールバック関数に任せている。
+ */
+export function removeDataFromGrid(
+ context: GridContext,
+ callback: (cell: GridCell) => void,
+) {
+ for (const cell of context.rangedCells) {
+ const { editable, events } = cell.column.setting;
+ if (editable) {
+ if (events?.delete) {
+ events.delete(cell, context);
+ } else {
+ callback(cell);
+ }
+ }
+ }
+}
diff --git a/packages/frontend/src/components/grid/grid.ts b/packages/frontend/src/components/grid/grid.ts
new file mode 100644
index 0000000000..0cb3b6f28b
--- /dev/null
+++ b/packages/frontend/src/components/grid/grid.ts
@@ -0,0 +1,44 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { EventEmitter } from 'eventemitter3';
+import { CellValue, GridCellSetting } from '@/components/grid/cell.js';
+import { GridColumnSetting } from '@/components/grid/column.js';
+import { GridRowSetting } from '@/components/grid/row.js';
+
+export type GridSetting = {
+ row?: GridRowSetting;
+ cols: GridColumnSetting[];
+ cells?: GridCellSetting;
+};
+
+export type DataSource = Record;
+
+export type GridState =
+ 'normal' |
+ 'cellSelecting' |
+ 'cellEditing' |
+ 'colResizing' |
+ 'colSelecting' |
+ 'rowSelecting' |
+ 'hidden'
+ ;
+
+export type Size = {
+ width: number;
+ height: number;
+}
+
+export type SizeStyle = number | 'auto' | undefined;
+
+export type AdditionalStyle = {
+ className?: string;
+ style?: Record;
+}
+
+export class GridEventEmitter extends EventEmitter<{
+ 'forceRefreshContentSize': void;
+}> {
+}
diff --git a/packages/frontend/src/components/grid/row.ts b/packages/frontend/src/components/grid/row.ts
new file mode 100644
index 0000000000..e0a317c9d3
--- /dev/null
+++ b/packages/frontend/src/components/grid/row.ts
@@ -0,0 +1,68 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { AdditionalStyle } from '@/components/grid/grid.js';
+import { GridCell } from '@/components/grid/cell.js';
+import { GridColumn } from '@/components/grid/column.js';
+import { MenuItem } from '@/types/menu.js';
+import { GridContext } from '@/components/grid/grid-event.js';
+
+export const defaultGridRowSetting: Required = {
+ showNumber: true,
+ selectable: true,
+ minimumDefinitionCount: 100,
+ styleRules: [],
+ contextMenuFactory: () => [],
+ events: {},
+};
+
+export type GridRowStyleRuleConditionParams = {
+ row: GridRow,
+ targetCols: GridColumn[],
+ cells: GridCell[]
+};
+
+export type GridRowStyleRule = {
+ condition: (params: GridRowStyleRuleConditionParams) => boolean;
+ applyStyle: AdditionalStyle;
+}
+
+export type GridRowContextMenuFactory = (row: GridRow, context: GridContext) => MenuItem[];
+
+export type GridRowSetting = {
+ showNumber?: boolean;
+ selectable?: boolean;
+ minimumDefinitionCount?: number;
+ styleRules?: GridRowStyleRule[];
+ contextMenuFactory?: GridRowContextMenuFactory;
+ events?: {
+ delete?: (rows: GridRow[]) => void;
+ }
+}
+
+export type GridRow = {
+ index: number;
+ ranged: boolean;
+ using: boolean;
+ setting: GridRowSetting;
+ additionalStyles: AdditionalStyle[];
+}
+
+export function createRow(index: number, using: boolean, setting: GridRowSetting): GridRow {
+ return {
+ index,
+ ranged: false,
+ using: using,
+ setting,
+ additionalStyles: [],
+ };
+}
+
+export function resetRow(row: GridRow): void {
+ row.ranged = false;
+ row.using = false;
+ row.additionalStyles = [];
+}
+
diff --git a/packages/frontend/src/components/hook/useLoading.ts b/packages/frontend/src/components/hook/useLoading.ts
new file mode 100644
index 0000000000..6c6ff6ae0d
--- /dev/null
+++ b/packages/frontend/src/components/hook/useLoading.ts
@@ -0,0 +1,52 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { computed, h, ref } from 'vue';
+import MkLoading from '@/components/global/MkLoading.vue';
+
+export const useLoading = (props?: {
+ static?: boolean;
+ inline?: boolean;
+ colored?: boolean;
+ mini?: boolean;
+ em?: boolean;
+}) => {
+ const showingCnt = ref(0);
+
+ const show = () => {
+ showingCnt.value++;
+ };
+
+ const close = (force?: boolean) => {
+ if (force) {
+ showingCnt.value = 0;
+ } else {
+ showingCnt.value = Math.max(0, showingCnt.value - 1);
+ }
+ };
+
+ const scope = (fn: () => T) => {
+ show();
+
+ const result = fn();
+ if (result instanceof Promise) {
+ return result.finally(() => close());
+ } else {
+ close();
+ return result;
+ }
+ };
+
+ const showing = computed(() => showingCnt.value > 0);
+ const component = computed(() => showing.value ? h(MkLoading, props) : null);
+
+ return {
+ show,
+ close,
+ scope,
+ component,
+ showing,
+ };
+};
diff --git a/packages/frontend/src/index.html b/packages/frontend/src/index.html
index 0be589262f..84ba9dfabc 100644
--- a/packages/frontend/src/index.html
+++ b/packages/frontend/src/index.html
@@ -20,6 +20,7 @@
worker-src 'self';
script-src 'self' 'unsafe-eval' https://*.hcaptcha.com https://*.recaptcha.net https://*.gstatic.com https://challenges.cloudflare.com https://esm.sh;
style-src 'self' 'unsafe-inline';
+ font-src 'self' data:;
img-src 'self' data: blob: 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 https://newassets.hcaptcha.com;
diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts
index 589ace0155..18c7464d2e 100644
--- a/packages/frontend/src/os.ts
+++ b/packages/frontend/src/os.ts
@@ -602,6 +602,27 @@ export async function selectDriveFolder(multiple: boolean): Promise {
+ return new Promise((resolve) => {
+ popup(defineAsyncComponent(() => import('@/components/MkRoleSelectDialog.vue')), params, {
+ done: roles => {
+ resolve({ canceled: false, result: roles });
+ },
+ close: () => {
+ resolve({ canceled: true, result: undefined });
+ },
+ }, 'dispose');
+ });
+}
+
export async function pickEmoji(src: HTMLElement, opts: ComponentProps): Promise {
return new Promise(resolve => {
const { dispose } = popup(MkEmojiPickerDialog, {
diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.impl.ts b/packages/frontend/src/pages/admin/custom-emojis-manager.impl.ts
new file mode 100644
index 0000000000..de2b2aca8c
--- /dev/null
+++ b/packages/frontend/src/pages/admin/custom-emojis-manager.impl.ts
@@ -0,0 +1,56 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export type RequestLogItem = {
+ failed: boolean;
+ url: string;
+ name: string;
+ error?: string;
+};
+
+export const gridSortOrderKeys = [
+ 'name',
+ 'category',
+ 'aliases',
+ 'type',
+ 'license',
+ 'host',
+ 'uri',
+ 'publicUrl',
+ 'isSensitive',
+ 'localOnly',
+ 'updatedAt',
+];
+export type GridSortOrderKey = typeof gridSortOrderKeys[number];
+
+export function emptyStrToUndefined(value: string | null) {
+ return value ? value : undefined;
+}
+
+export function emptyStrToNull(value: string) {
+ return value === '' ? null : value;
+}
+
+export function emptyStrToEmptyArray(value: string) {
+ return value === '' ? [] : value.split(' ').map(it => it.trim());
+}
+
+export function roleIdsParser(text: string): { id: string, name: string }[] {
+ // idとnameのペア配列をJSONで受け取る。それ以外の形式は許容しない
+ try {
+ const obj = JSON.parse(text);
+ if (!Array.isArray(obj)) {
+ return [];
+ }
+ if (!obj.every(it => typeof it === 'object' && 'id' in it && 'name' in it)) {
+ return [];
+ }
+
+ return obj.map(it => ({ id: it.id, name: it.name }));
+ } catch (ex) {
+ console.warn(ex);
+ return [];
+ }
+}
diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.vue
new file mode 100644
index 0000000000..55f9632ce4
--- /dev/null
+++ b/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.vue
@@ -0,0 +1,757 @@
+
+
+
+
+
+
+
+
+ {{ i18n.ts._customEmojisManager._gridCommon.searchSettings }}
+
+ {{ i18n.ts._customEmojisManager._gridCommon.searchSettingCaption }}
+
+
+
+
+
+ name
+
+
+ category
+
+
+ aliases
+
+
+
+ type
+
+
+ license
+
+
+ sensitive
+ -
+ true
+ false
+
+
+
+ localOnly
+ -
+ true
+ false
+
+
+ updatedAt(from)
+
+
+ updatedAt(to)
+
+
+
+ role
+
+
+
+
+
+
+ {{ i18n.ts._customEmojisManager._gridCommon.sortOrder }}
+
+
+
+
+
+ {{ i18n.ts.search }}
+
+
+ {{ i18n.ts.reset }}
+
+
+
+
+
+
+
+
+
+
+ {{ i18n.ts._customEmojisManager._local._list.emojisNothing }}
+
+
+
+
+
+
+
+
+
+
+ {{ i18n.ts.delete }} ({{ deleteItemsCount }})
+
+
+
+
+
+
+
+
+
+ {{ i18n.ts.update }} ({{ updatedItemsCount }})
+
+ {{ i18n.ts.reset }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.local.register.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.local.register.vue
new file mode 100644
index 0000000000..a3de5de569
--- /dev/null
+++ b/packages/frontend/src/pages/admin/custom-emojis-manager.local.register.vue
@@ -0,0 +1,477 @@
+
+
+
+
+
+
+ {{ i18n.ts._customEmojisManager._local._register.uploadSettingTitle }}
+ {{ i18n.ts._customEmojisManager._local._register.uploadSettingDescription }}
+
+
+
+ {{ i18n.ts.uploadFolder }}
+
+ {{ folder.name }}
+
+
+
+
+ {{ i18n.ts.keepOriginalUploading }}
+ {{ i18n.ts.keepOriginalUploadingDescription }}
+
+
+
+ {{ i18n.ts._customEmojisManager._local._register.directoryToCategoryLabel }}
+ {{ i18n.ts._customEmojisManager._local._register.directoryToCategoryCaption }}
+
+
+
+
+
+
+
+
+ {{ i18n.ts._customEmojisManager._local._register.emojiInputAreaCaption }}
+
+
+
+
+
+
+
+
+
+
+ {{ i18n.ts.registration }}
+
+
+ {{ i18n.ts.clear }}
+
+
+
+
+
+
+
+
diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.local.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.local.vue
new file mode 100644
index 0000000000..ea4303f342
--- /dev/null
+++ b/packages/frontend/src/pages/admin/custom-emojis-manager.local.vue
@@ -0,0 +1,36 @@
+
+
+
+
+
+ {{ i18n.ts._customEmojisManager._local.tabTitleList }}
+ {{ i18n.ts._customEmojisManager._local.tabTitleRegister }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.logs-folder.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.logs-folder.vue
new file mode 100644
index 0000000000..f75f6c0da5
--- /dev/null
+++ b/packages/frontend/src/pages/admin/custom-emojis-manager.logs-folder.vue
@@ -0,0 +1,102 @@
+
+
+
+
+
+ {{ i18n.ts._customEmojisManager._gridCommon.registrationLogs }}
+
+ {{ i18n.ts._customEmojisManager._gridCommon.registrationLogsCaption }}
+
+
+
+
+
+ {{ i18n.ts._customEmojisManager._logs.showSuccessLogSwitch }}
+
+
+
+
+
+
+ {{ i18n.ts._customEmojisManager._logs.failureLogNothing }}
+
+
+
+
+ {{ i18n.ts._customEmojisManager._logs.logNothing }}
+
+
+
+
+
+
+
+
diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.remote.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.remote.vue
new file mode 100644
index 0000000000..9a9d2990ba
--- /dev/null
+++ b/packages/frontend/src/pages/admin/custom-emojis-manager.remote.vue
@@ -0,0 +1,441 @@
+
+
+
+
+
+
+
+
+ {{ i18n.ts._customEmojisManager._gridCommon.searchSettings }}
+
+ {{ i18n.ts._customEmojisManager._gridCommon.searchSettingCaption }}
+
+
+
+
+
+ name
+
+
+ host
+
+
+ uri
+
+
+ publicUrl
+
+
+
+
+
+ {{ i18n.ts._customEmojisManager._gridCommon.sortOrder }}
+
+
+
+
+
+ {{ i18n.ts.search }}
+
+
+ {{ i18n.ts.reset }}
+
+
+
+
+
+
+
+
+
+
+ {{ i18n.ts._customEmojisManager._local._list.emojisNothing }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{
+ i18n.ts._customEmojisManager._remote.importEmojisButton
+ }} ({{ checkedItemsCount }})
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager2.stories.impl.ts b/packages/frontend/src/pages/admin/custom-emojis-manager2.stories.impl.ts
new file mode 100644
index 0000000000..f62304277a
--- /dev/null
+++ b/packages/frontend/src/pages/admin/custom-emojis-manager2.stories.impl.ts
@@ -0,0 +1,160 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { delay, http, HttpResponse } from 'msw';
+import { StoryObj } from '@storybook/vue3';
+import { entities } from 'misskey-js';
+import { commonHandlers } from '../../../.storybook/mocks.js';
+import { emoji } from '../../../.storybook/fakes.js';
+import { fakeId } from '../../../.storybook/fake-utils.js';
+import custom_emojis_manager2 from './custom-emojis-manager2.vue';
+
+function createRender(params: {
+ emojis: entities.EmojiDetailedAdmin[];
+}) {
+ const storedEmojis: entities.EmojiDetailedAdmin[] = [...params.emojis];
+ const storedDriveFiles: entities.DriveFile[] = [];
+
+ return {
+ render(args) {
+ return {
+ components: {
+ custom_emojis_manager2,
+ },
+ setup() {
+ return {
+ args,
+ };
+ },
+ computed: {
+ props() {
+ return {
+ ...this.args,
+ };
+ },
+ },
+ template: ' ',
+ };
+ },
+ args: {
+
+ },
+ parameters: {
+ layout: 'fullscreen',
+ msw: {
+ handlers: [
+ ...commonHandlers,
+ http.post('/api/v2/admin/emoji/list', async ({ request }) => {
+ await delay(100);
+
+ const bodyStream = request.body as ReadableStream;
+ const body = await new Response(bodyStream).json() as entities.V2AdminEmojiListRequest;
+
+ const emojis = storedEmojis;
+ const limit = body.limit ?? 10;
+ const page = body.page ?? 1;
+ const result = emojis.slice((page - 1) * limit, page * limit);
+
+ return HttpResponse.json({
+ emojis: result,
+ count: Math.min(emojis.length, limit),
+ allCount: emojis.length,
+ allPages: Math.ceil(emojis.length / limit),
+ });
+ }),
+ http.post('/api/drive/folders', () => {
+ return HttpResponse.json([]);
+ }),
+ http.post('/api/drive/files', () => {
+ return HttpResponse.json(storedDriveFiles);
+ }),
+ http.post('/api/drive/files/create', async ({ request }) => {
+ const data = await request.formData();
+ const file = data.get('file');
+ if (!file || !(file instanceof File)) {
+ return HttpResponse.json({ error: 'file is required' }, {
+ status: 400,
+ });
+ }
+
+ // FIXME: ファイルのバイナリに0xEF 0xBF 0xBDが混入してしまい、うまく画像ファイルとして表示できない問題がある
+ const base64 = await new Promise((resolve) => {
+ const reader = new FileReader();
+ reader.onload = () => {
+ resolve(reader.result as string);
+ };
+ reader.readAsDataURL(new Blob([file], { type: 'image/webp' }));
+ });
+
+ const driveFile: entities.DriveFile = {
+ id: fakeId(file.name),
+ createdAt: new Date().toISOString(),
+ name: file.name,
+ type: file.type,
+ md5: '',
+ size: file.size,
+ isSensitive: false,
+ blurhash: null,
+ properties: {},
+ url: base64,
+ thumbnailUrl: null,
+ comment: null,
+ folderId: null,
+ folder: null,
+ userId: null,
+ user: null,
+ };
+
+ storedDriveFiles.push(driveFile);
+
+ return HttpResponse.json(driveFile);
+ }),
+ http.post('api/admin/emoji/add', async ({ request }) => {
+ await delay(100);
+
+ const bodyStream = request.body as ReadableStream;
+ const body = await new Response(bodyStream).json() as entities.AdminEmojiAddRequest;
+
+ const fileId = body.fileId;
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ const file = storedDriveFiles.find(f => f.id === fileId)!;
+
+ const em = emoji({
+ id: fakeId(file.name),
+ name: body.name,
+ publicUrl: file.url,
+ originalUrl: file.url,
+ type: file.type,
+ aliases: body.aliases,
+ category: body.category ?? undefined,
+ license: body.license ?? undefined,
+ localOnly: body.localOnly,
+ isSensitive: body.isSensitive,
+ });
+ storedEmojis.push(em);
+
+ return HttpResponse.json(null);
+ }),
+ ],
+ },
+ },
+ } satisfies StoryObj;
+}
+
+export const Default = createRender({
+ emojis: [],
+});
+
+export const List10 = createRender({
+ emojis: Array.from({ length: 10 }, (_, i) => emoji({ name: `emoji_${i}` }, i.toString())),
+});
+
+export const List100 = createRender({
+ emojis: Array.from({ length: 100 }, (_, i) => emoji({ name: `emoji_${i}` }, i.toString())),
+});
+
+export const List1000 = createRender({
+ emojis: Array.from({ length: 1000 }, (_, i) => emoji({ name: `emoji_${i}` }, i.toString())),
+});
diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager2.vue b/packages/frontend/src/pages/admin/custom-emojis-manager2.vue
new file mode 100644
index 0000000000..a952a5a3d1
--- /dev/null
+++ b/packages/frontend/src/pages/admin/custom-emojis-manager2.vue
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend/src/pages/admin/index.vue b/packages/frontend/src/pages/admin/index.vue
index fd15ae1d66..969ca8b9e8 100644
--- a/packages/frontend/src/pages/admin/index.vue
+++ b/packages/frontend/src/pages/admin/index.vue
@@ -121,6 +121,11 @@ const menuDef = computed(() => [{
text: i18n.ts.customEmojis,
to: '/admin/emojis',
active: currentPage.value?.route.name === 'emojis',
+ }, {
+ icon: 'ti ti-icons',
+ text: i18n.ts.customEmojis + '(beta)',
+ to: '/admin/emojis2',
+ active: currentPage.value?.route.name === 'emojis2',
}, {
icon: 'ti ti-sparkles',
text: i18n.ts.avatarDecorations,
diff --git a/packages/frontend/src/router/definition.ts b/packages/frontend/src/router/definition.ts
index e98e0b59b1..732b209a36 100644
--- a/packages/frontend/src/router/definition.ts
+++ b/packages/frontend/src/router/definition.ts
@@ -382,6 +382,10 @@ const routes: RouteDef[] = [{
path: '/emojis',
name: 'emojis',
component: page(() => import('@/pages/custom-emojis-manager.vue')),
+ }, {
+ path: '/emojis2',
+ name: 'emojis2',
+ component: page(() => import('@/pages/admin/custom-emojis-manager2.vue')),
}, {
path: '/avatar-decorations',
name: 'avatarDecorations',
diff --git a/packages/frontend/src/scripts/file-drop.ts b/packages/frontend/src/scripts/file-drop.ts
new file mode 100644
index 0000000000..c2e863c0dc
--- /dev/null
+++ b/packages/frontend/src/scripts/file-drop.ts
@@ -0,0 +1,121 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export type DroppedItem = DroppedFile | DroppedDirectory;
+
+export type DroppedFile = {
+ isFile: true;
+ path: string;
+ file: File;
+};
+
+export type DroppedDirectory = {
+ isFile: false;
+ path: string;
+ children: DroppedItem[];
+}
+
+export async function extractDroppedItems(ev: DragEvent): Promise {
+ const dropItems = ev.dataTransfer?.items;
+ if (!dropItems || dropItems.length === 0) {
+ return [];
+ }
+
+ const apiTestItem = dropItems[0];
+ if ('webkitGetAsEntry' in apiTestItem) {
+ return readDataTransferItems(dropItems);
+ } else {
+ // webkitGetAsEntryに対応していない場合はfilesから取得する(ディレクトリのサポートは出来ない)
+ const dropFiles = ev.dataTransfer.files;
+ if (dropFiles.length === 0) {
+ return [];
+ }
+
+ const droppedFiles = Array.of();
+ for (let i = 0; i < dropFiles.length; i++) {
+ const file = dropFiles.item(i);
+ if (file) {
+ droppedFiles.push({
+ isFile: true,
+ path: file.name,
+ file,
+ });
+ }
+ }
+
+ return droppedFiles;
+ }
+}
+
+/**
+ * ドラッグ&ドロップされたファイルのリストからディレクトリ構造とファイルへの参照({@link File})を取得する。
+ */
+export async function readDataTransferItems(itemList: DataTransferItemList): Promise {
+ async function readEntry(entry: FileSystemEntry): Promise {
+ if (entry.isFile) {
+ return {
+ isFile: true,
+ path: entry.fullPath,
+ file: await readFile(entry as FileSystemFileEntry),
+ };
+ } else {
+ return {
+ isFile: false,
+ path: entry.fullPath,
+ children: await readDirectory(entry as FileSystemDirectoryEntry),
+ };
+ }
+ }
+
+ function readFile(fileSystemFileEntry: FileSystemFileEntry): Promise {
+ return new Promise((resolve, reject) => {
+ fileSystemFileEntry.file(resolve, reject);
+ });
+ }
+
+ function readDirectory(fileSystemDirectoryEntry: FileSystemDirectoryEntry): Promise {
+ return new Promise(async (resolve) => {
+ const allEntries = Array.of();
+ const reader = fileSystemDirectoryEntry.createReader();
+ while (true) {
+ const entries = await new Promise((res, rej) => reader.readEntries(res, rej));
+ if (entries.length === 0) {
+ break;
+ }
+ allEntries.push(...entries);
+ }
+
+ resolve(await Promise.all(allEntries.map(readEntry)));
+ });
+ }
+
+ // 扱いにくいので配列に変換
+ const items = Array.of();
+ for (let i = 0; i < itemList.length; i++) {
+ items.push(itemList[i]);
+ }
+
+ return Promise.all(
+ items
+ .map(it => it.webkitGetAsEntry())
+ .filter(it => it)
+ .map(it => readEntry(it!)),
+ );
+}
+
+/**
+ * {@link DroppedItem}のリストからディレクトリを再帰的に検索し、ファイルのリストを取得する。
+ */
+export function flattenDroppedFiles(items: DroppedItem[]): DroppedFile[] {
+ const result = Array.of();
+ for (const item of items) {
+ if (item.isFile) {
+ result.push(item);
+ } else {
+ result.push(...flattenDroppedFiles(item.children));
+ }
+ }
+ return result;
+}
diff --git a/packages/frontend/src/scripts/key-event.ts b/packages/frontend/src/scripts/key-event.ts
new file mode 100644
index 0000000000..a72776d48c
--- /dev/null
+++ b/packages/frontend/src/scripts/key-event.ts
@@ -0,0 +1,153 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+/**
+ * {@link KeyboardEvent.code} の値を表す文字列。不足分は適宜追加する
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_code_values
+ */
+export type KeyCode =
+ | 'Backspace'
+ | 'Tab'
+ | 'Enter'
+ | 'Shift'
+ | 'Control'
+ | 'Alt'
+ | 'Pause'
+ | 'CapsLock'
+ | 'Escape'
+ | 'Space'
+ | 'PageUp'
+ | 'PageDown'
+ | 'End'
+ | 'Home'
+ | 'ArrowLeft'
+ | 'ArrowUp'
+ | 'ArrowRight'
+ | 'ArrowDown'
+ | 'Insert'
+ | 'Delete'
+ | 'Digit0'
+ | 'Digit1'
+ | 'Digit2'
+ | 'Digit3'
+ | 'Digit4'
+ | 'Digit5'
+ | 'Digit6'
+ | 'Digit7'
+ | 'Digit8'
+ | 'Digit9'
+ | 'KeyA'
+ | 'KeyB'
+ | 'KeyC'
+ | 'KeyD'
+ | 'KeyE'
+ | 'KeyF'
+ | 'KeyG'
+ | 'KeyH'
+ | 'KeyI'
+ | 'KeyJ'
+ | 'KeyK'
+ | 'KeyL'
+ | 'KeyM'
+ | 'KeyN'
+ | 'KeyO'
+ | 'KeyP'
+ | 'KeyQ'
+ | 'KeyR'
+ | 'KeyS'
+ | 'KeyT'
+ | 'KeyU'
+ | 'KeyV'
+ | 'KeyW'
+ | 'KeyX'
+ | 'KeyY'
+ | 'KeyZ'
+ | 'MetaLeft'
+ | 'MetaRight'
+ | 'ContextMenu'
+ | 'F1'
+ | 'F2'
+ | 'F3'
+ | 'F4'
+ | 'F5'
+ | 'F6'
+ | 'F7'
+ | 'F8'
+ | 'F9'
+ | 'F10'
+ | 'F11'
+ | 'F12'
+ | 'NumLock'
+ | 'ScrollLock'
+ | 'Semicolon'
+ | 'Equal'
+ | 'Comma'
+ | 'Minus'
+ | 'Period'
+ | 'Slash'
+ | 'Backquote'
+ | 'BracketLeft'
+ | 'Backslash'
+ | 'BracketRight'
+ | 'Quote'
+ | 'Meta'
+ | 'AltGraph'
+ ;
+
+/**
+ * 修飾キーを表す文字列。不足分は適宜追加する。
+ */
+export type KeyModifier =
+ | 'Shift'
+ | 'Control'
+ | 'Alt'
+ | 'Meta'
+ ;
+
+/**
+ * 押下されたキー以外の状態を表す文字列。不足分は適宜追加する。
+ */
+export type KeyState =
+ | 'composing'
+ | 'repeat'
+ ;
+
+export type KeyEventHandler = {
+ modifiers?: KeyModifier[];
+ states?: KeyState[];
+ code: KeyCode | 'any';
+ handler: (event: KeyboardEvent) => void;
+}
+
+export function handleKeyEvent(event: KeyboardEvent, handlers: KeyEventHandler[]) {
+ function checkModifier(ev: KeyboardEvent, modifiers? : KeyModifier[]) {
+ if (modifiers) {
+ return modifiers.every(modifier => ev.getModifierState(modifier));
+ }
+ return true;
+ }
+
+ function checkState(ev: KeyboardEvent, states?: KeyState[]) {
+ if (states) {
+ return states.every(state => ev.getModifierState(state));
+ }
+ return true;
+ }
+
+ let hit = false;
+ for (const handler of handlers.filter(it => it.code === event.code)) {
+ if (checkModifier(event, handler.modifiers) && checkState(event, handler.states)) {
+ handler.handler(event);
+ hit = true;
+ break;
+ }
+ }
+
+ if (!hit) {
+ for (const handler of handlers.filter(it => it.code === 'any')) {
+ handler.handler(event);
+ }
+ }
+}
diff --git a/packages/frontend/src/scripts/select-file.ts b/packages/frontend/src/scripts/select-file.ts
index b037aa8acc..c25b4d73bd 100644
--- a/packages/frontend/src/scripts/select-file.ts
+++ b/packages/frontend/src/scripts/select-file.ts
@@ -12,14 +12,28 @@ import { i18n } from '@/i18n.js';
import { defaultStore } from '@/store.js';
import { uploadFile } from '@/scripts/upload.js';
-export function chooseFileFromPc(multiple: boolean, keepOriginal = false): Promise {
+export function chooseFileFromPc(
+ multiple: boolean,
+ options?: {
+ uploadFolder?: string | null;
+ keepOriginal?: boolean;
+ nameConverter?: (file: File) => string | undefined;
+ },
+): Promise {
+ const uploadFolder = options?.uploadFolder ?? defaultStore.state.uploadFolder;
+ const keepOriginal = options?.keepOriginal ?? defaultStore.state.keepOriginalUploading;
+ const nameConverter = options?.nameConverter ?? (() => undefined);
+
return new Promise((res, rej) => {
const input = document.createElement('input');
input.type = 'file';
input.multiple = multiple;
input.onchange = () => {
if (!input.files) return res([]);
- const promises = Array.from(input.files, file => uploadFile(file, defaultStore.state.uploadFolder, undefined, keepOriginal));
+ const promises = Array.from(
+ input.files,
+ file => uploadFile(file, uploadFolder, nameConverter(file), keepOriginal),
+ );
Promise.all(promises).then(driveFiles => {
res(driveFiles);
@@ -94,7 +108,7 @@ function select(src: HTMLElement | EventTarget | null, label: string | null, mul
}, {
text: i18n.ts.upload,
icon: 'ti ti-upload',
- action: () => chooseFileFromPc(multiple, keepOriginal.value).then(files => res(files)),
+ action: () => chooseFileFromPc(multiple, { keepOriginal: keepOriginal.value }).then(files => res(files)),
}, {
text: i18n.ts.fromDrive,
icon: 'ti ti-cloud',
diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md
index 211ddb8287..7098b52205 100644
--- a/packages/misskey-js/etc/misskey-js.api.md
+++ b/packages/misskey-js/etc/misskey-js.api.md
@@ -1117,6 +1117,9 @@ type EmojiDeleted = {
// @public (undocumented)
type EmojiDetailed = components['schemas']['EmojiDetailed'];
+// @public (undocumented)
+type EmojiDetailedAdmin = components['schemas']['EmojiDetailedAdmin'];
+
// @public (undocumented)
type EmojiRequest = operations['emoji']['requestBody']['content']['application/json'];
@@ -1294,6 +1297,8 @@ declare namespace entities {
AdminEmojiSetCategoryBulkRequest,
AdminEmojiSetLicenseBulkRequest,
AdminEmojiUpdateRequest,
+ V2AdminEmojiListRequest,
+ V2AdminEmojiListResponse,
AdminFederationDeleteAllFilesRequest,
AdminFederationRefreshRemoteInstanceMetadataRequest,
AdminFederationRemoveAllFollowingRequest,
@@ -1847,6 +1852,7 @@ declare namespace entities {
GalleryPost,
EmojiSimple,
EmojiDetailed,
+ EmojiDetailedAdmin,
Flash,
Signin,
RoleCondFormulaLogics,
@@ -3420,6 +3426,12 @@ type UsersShowResponse = operations['users___show']['responses']['200']['content
// @public (undocumented)
type UsersUpdateMemoRequest = operations['users___update-memo']['requestBody']['content']['application/json'];
+// @public (undocumented)
+type V2AdminEmojiListRequest = operations['v2___admin___emoji___list']['requestBody']['content']['application/json'];
+
+// @public (undocumented)
+type V2AdminEmojiListResponse = operations['v2___admin___emoji___list']['responses']['200']['content']['application/json'];
+
// Warnings were encountered during analysis:
//
// src/entities.ts:50:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts
diff --git a/packages/misskey-js/src/autogen/apiClientJSDoc.ts b/packages/misskey-js/src/autogen/apiClientJSDoc.ts
index 3bcdae6a4a..edaa0498e9 100644
--- a/packages/misskey-js/src/autogen/apiClientJSDoc.ts
+++ b/packages/misskey-js/src/autogen/apiClientJSDoc.ts
@@ -493,6 +493,17 @@ declare module '../api.js' {
credential?: string | null,
): Promise>;
+ /**
+ * No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *read:admin:emoji*
+ */
+ request(
+ endpoint: E,
+ params: P,
+ credential?: string | null,
+ ): Promise>;
+
/**
* No description provided.
*
diff --git a/packages/misskey-js/src/autogen/endpoint.ts b/packages/misskey-js/src/autogen/endpoint.ts
index b016d5bbcf..982717597b 100644
--- a/packages/misskey-js/src/autogen/endpoint.ts
+++ b/packages/misskey-js/src/autogen/endpoint.ts
@@ -62,6 +62,8 @@ import type {
AdminEmojiSetCategoryBulkRequest,
AdminEmojiSetLicenseBulkRequest,
AdminEmojiUpdateRequest,
+ V2AdminEmojiListRequest,
+ V2AdminEmojiListResponse,
AdminFederationDeleteAllFilesRequest,
AdminFederationRefreshRemoteInstanceMetadataRequest,
AdminFederationRemoveAllFollowingRequest,
@@ -628,6 +630,7 @@ export type Endpoints = {
'admin/emoji/set-category-bulk': { req: AdminEmojiSetCategoryBulkRequest; res: EmptyResponse };
'admin/emoji/set-license-bulk': { req: AdminEmojiSetLicenseBulkRequest; res: EmptyResponse };
'admin/emoji/update': { req: AdminEmojiUpdateRequest; res: EmptyResponse };
+ 'v2/admin/emoji/list': { req: V2AdminEmojiListRequest; res: V2AdminEmojiListResponse };
'admin/federation/delete-all-files': { req: AdminFederationDeleteAllFilesRequest; res: EmptyResponse };
'admin/federation/refresh-remote-instance-metadata': { req: AdminFederationRefreshRemoteInstanceMetadataRequest; res: EmptyResponse };
'admin/federation/remove-all-following': { req: AdminFederationRemoveAllFollowingRequest; res: EmptyResponse };
diff --git a/packages/misskey-js/src/autogen/entities.ts b/packages/misskey-js/src/autogen/entities.ts
index 02be4848c7..e4299d62c7 100644
--- a/packages/misskey-js/src/autogen/entities.ts
+++ b/packages/misskey-js/src/autogen/entities.ts
@@ -65,6 +65,8 @@ export type AdminEmojiSetAliasesBulkRequest = operations['admin___emoji___set-al
export type AdminEmojiSetCategoryBulkRequest = operations['admin___emoji___set-category-bulk']['requestBody']['content']['application/json'];
export type AdminEmojiSetLicenseBulkRequest = operations['admin___emoji___set-license-bulk']['requestBody']['content']['application/json'];
export type AdminEmojiUpdateRequest = operations['admin___emoji___update']['requestBody']['content']['application/json'];
+export type V2AdminEmojiListRequest = operations['v2___admin___emoji___list']['requestBody']['content']['application/json'];
+export type V2AdminEmojiListResponse = operations['v2___admin___emoji___list']['responses']['200']['content']['application/json'];
export type AdminFederationDeleteAllFilesRequest = operations['admin___federation___delete-all-files']['requestBody']['content']['application/json'];
export type AdminFederationRefreshRemoteInstanceMetadataRequest = operations['admin___federation___refresh-remote-instance-metadata']['requestBody']['content']['application/json'];
export type AdminFederationRemoveAllFollowingRequest = operations['admin___federation___remove-all-following']['requestBody']['content']['application/json'];
diff --git a/packages/misskey-js/src/autogen/models.ts b/packages/misskey-js/src/autogen/models.ts
index 04574849d4..1a30da4437 100644
--- a/packages/misskey-js/src/autogen/models.ts
+++ b/packages/misskey-js/src/autogen/models.ts
@@ -33,6 +33,7 @@ export type FederationInstance = components['schemas']['FederationInstance'];
export type GalleryPost = components['schemas']['GalleryPost'];
export type EmojiSimple = components['schemas']['EmojiSimple'];
export type EmojiDetailed = components['schemas']['EmojiDetailed'];
+export type EmojiDetailedAdmin = components['schemas']['EmojiDetailedAdmin'];
export type Flash = components['schemas']['Flash'];
export type Signin = components['schemas']['Signin'];
export type RoleCondFormulaLogics = components['schemas']['RoleCondFormulaLogics'];
diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts
index ada685604d..75a99263d0 100644
--- a/packages/misskey-js/src/autogen/types.ts
+++ b/packages/misskey-js/src/autogen/types.ts
@@ -414,6 +414,15 @@ export type paths = {
*/
post: operations['admin___emoji___update'];
};
+ '/v2/admin/emoji/list': {
+ /**
+ * v2/admin/emoji/list
+ * @description No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *read:admin:emoji*
+ */
+ post: operations['v2___admin___emoji___list'];
+ };
'/admin/federation/delete-all-files': {
/**
* admin/federation/delete-all-files
@@ -4749,6 +4758,29 @@ export type components = {
localOnly: boolean;
roleIdsThatCanBeUsedThisEmojiAsReaction: string[];
};
+ EmojiDetailedAdmin: {
+ /** Format: id */
+ id: string;
+ /** Format: date-time */
+ updatedAt: string | null;
+ name: string;
+ /** @description The local host is represented with `null`. */
+ host: string | null;
+ publicUrl: string;
+ originalUrl: string;
+ uri: string | null;
+ type: string | null;
+ aliases: string[];
+ category: string | null;
+ license: string | null;
+ localOnly: boolean;
+ isSensitive: boolean;
+ roleIdsThatCanBeUsedThisEmojiAsReaction: {
+ /** Format: misskey:id */
+ id: string;
+ name: string;
+ }[];
+ };
Flash: {
/**
* Format: id
@@ -7872,6 +7904,97 @@ export type operations = {
};
};
};
+ /**
+ * v2/admin/emoji/list
+ * @description No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *read:admin:emoji*
+ */
+ v2___admin___emoji___list: {
+ requestBody: {
+ content: {
+ 'application/json': {
+ query?: ({
+ updatedAtFrom?: string;
+ updatedAtTo?: string;
+ name?: string;
+ host?: string;
+ uri?: string;
+ publicUrl?: string;
+ originalUrl?: string;
+ type?: string;
+ aliases?: string;
+ category?: string;
+ license?: string;
+ isSensitive?: boolean;
+ localOnly?: boolean;
+ /**
+ * @default all
+ * @enum {string}
+ */
+ hostType?: 'local' | 'remote' | 'all';
+ roleIds?: string[];
+ }) | null;
+ /** Format: misskey:id */
+ sinceId?: string;
+ /** Format: misskey:id */
+ untilId?: string;
+ /** @default 10 */
+ limit?: number;
+ page?: number;
+ /**
+ * @default [
+ * "-id"
+ * ]
+ */
+ sortKeys?: ('+id' | '-id' | '+updatedAt' | '-updatedAt' | '+name' | '-name' | '+host' | '-host' | '+uri' | '-uri' | '+publicUrl' | '-publicUrl' | '+type' | '-type' | '+aliases' | '-aliases' | '+category' | '-category' | '+license' | '-license' | '+isSensitive' | '-isSensitive' | '+localOnly' | '-localOnly' | '+roleIdsThatCanBeUsedThisEmojiAsReaction' | '-roleIdsThatCanBeUsedThisEmojiAsReaction')[];
+ };
+ };
+ };
+ responses: {
+ /** @description OK (with results) */
+ 200: {
+ content: {
+ 'application/json': {
+ emojis: components['schemas']['EmojiDetailedAdmin'][];
+ count: number;
+ allCount: number;
+ allPages: number;
+ };
+ };
+ };
+ /** @description Client error */
+ 400: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Authentication error */
+ 401: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Forbidden error */
+ 403: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description I'm Ai */
+ 418: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ };
+ };
/**
* admin/federation/delete-all-files
* @description No description provided.
--
cgit v1.2.3-freya
From 68175bc38d80c96a47fe731b42c35a2124a748d6 Mon Sep 17 00:00:00 2001
From: anatawa12
Date: Tue, 21 Jan 2025 10:02:27 +0900
Subject: enhance(frontend): クエリパラメータでuiを一時的に変更できるように
(#15240)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* feat: クエリパラメータでuiを一時的に変更できるように
* docs(changelog): クエリパラメータでuiを一時的に変更できるように
* Update packages/frontend/src/boot/main-boot.ts
Co-authored-by: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>
---------
Co-authored-by: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>
---
CHANGELOG.md | 1 +
packages/frontend/src/boot/main-boot.ts | 40 +++++++++++++++++++++++++++------
2 files changed, 34 insertions(+), 7 deletions(-)
(limited to 'packages/frontend/src')
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2fa49e3456..5b7ae08215 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -21,6 +21,7 @@
- Enhance: ノートの添付ファイルを一覧で遡れる「ファイル」タブを追加
(Based on https://github.com/Otaku-Social/maniakey/pull/14)
- Enhance: AiScriptの拡張API関数において引数の型チェックをより厳格に
+- Enhance: クエリパラメータでuiを一時的に変更できるように #15240
- Fix: 画面サイズが変わった際にナビゲーションバーが自動で折りたたまれない問題を修正
- Fix: サーバー情報メニューに区切り線が不足していたのを修正
- Fix: ノートがログインしているユーザーしか見れない場合にログインダイアログを閉じるとその後の動線がなくなる問題を修正
diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts
index 2bf9029479..874e97f3a4 100644
--- a/packages/frontend/src/boot/main-boot.ts
+++ b/packages/frontend/src/boot/main-boot.ts
@@ -7,6 +7,7 @@ import { createApp, defineAsyncComponent, markRaw } from 'vue';
import { ui } from '@@/js/config.js';
import { common } from './common.js';
import type * as Misskey from 'misskey-js';
+import type { Component } from 'vue';
import { i18n } from '@/i18n.js';
import { alert, confirm, popup, post, toast } from '@/os.js';
import { useStream } from '@/stream.js';
@@ -25,13 +26,38 @@ import { type Keymap, makeHotkey } from '@/scripts/hotkey.js';
import { addCustomEmoji, removeCustomEmojis, updateCustomEmojis } from '@/custom-emojis.js';
export async function mainBoot() {
- const { isClientUpdated } = await common(() => createApp(
- new URLSearchParams(window.location.search).has('zen') || (ui === 'deck' && deckStore.state.useSimpleUiForNonRootPages && location.pathname !== '/') ? defineAsyncComponent(() => import('@/ui/zen.vue')) :
- !$i ? defineAsyncComponent(() => import('@/ui/visitor.vue')) :
- ui === 'deck' ? defineAsyncComponent(() => import('@/ui/deck.vue')) :
- ui === 'classic' ? defineAsyncComponent(() => import('@/ui/classic.vue')) :
- defineAsyncComponent(() => import('@/ui/universal.vue')),
- ));
+ const { isClientUpdated } = await common(() => {
+ let uiStyle = ui;
+ const searchParams = new URLSearchParams(window.location.search);
+
+ if (!$i) uiStyle = 'visitor';
+
+ if (searchParams.has('zen')) uiStyle = 'zen';
+ if (uiStyle === 'deck' && deckStore.state.useSimpleUiForNonRootPages && location.pathname !== '/') uiStyle = 'zen';
+
+ if (searchParams.has('ui')) uiStyle = searchParams.get('ui');
+
+ let rootComponent: Component;
+ switch (uiStyle) {
+ case 'zen':
+ rootComponent = defineAsyncComponent(() => import('@/ui/zen.vue'));
+ break;
+ case 'deck':
+ rootComponent = defineAsyncComponent(() => import('@/ui/deck.vue'));
+ break;
+ case 'visitor':
+ rootComponent = defineAsyncComponent(() => import('@/ui/visitor.vue'));
+ break;
+ case 'classic':
+ rootComponent = defineAsyncComponent(() => import('@/ui/classic.vue'));
+ break;
+ default:
+ rootComponent = defineAsyncComponent(() => import('@/ui/universal.vue'));
+ break;
+ }
+
+ return createApp(rootComponent);
+ });
reactionPicker.init();
emojiPicker.init();
--
cgit v1.2.3-freya
From e8b633efec5a26b6534cd59a3ca1dd14899defd1 Mon Sep 17 00:00:00 2001
From: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Tue, 21 Jan 2025 10:26:47 +0900
Subject: fix(frontend):
Instanceの値が部分的に欠損していると、ローカルサーバーの情報にフォールバックする問題を修正
(#15319)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
packages/frontend/src/components/MkInstanceTicker.vue | 19 ++++++++++++++++---
packages/frontend/src/components/MkNote.vue | 2 +-
packages/frontend/src/components/MkNoteDetailed.vue | 2 +-
3 files changed, 18 insertions(+), 5 deletions(-)
(limited to 'packages/frontend/src')
diff --git a/packages/frontend/src/components/MkInstanceTicker.vue b/packages/frontend/src/components/MkInstanceTicker.vue
index 570c50b627..70c33a692d 100644
--- a/packages/frontend/src/components/MkInstanceTicker.vue
+++ b/packages/frontend/src/components/MkInstanceTicker.vue
@@ -17,6 +17,7 @@ import { instance as localInstance } from '@/instance.js';
import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js';
const props = defineProps<{
+ host: string | null;
instance?: {
faviconUrl?: string | null
name?: string | null
@@ -25,12 +26,24 @@ const props = defineProps<{
}>();
// if no instance data is given, this is for the local instance
-const instanceName = computed(() => props.instance?.name ?? localInstanceName);
+const instanceName = computed(() => props.host == null ? localInstanceName : props.instance?.name ?? props.host);
-const faviconUrl = computed(() => getProxiedImageUrlNullable(props.instance?.faviconUrl ?? localInstance.iconUrl, 'preview') ?? '/favicon.ico');
+const faviconUrl = computed(() => {
+ let imageSrc: string | null = null;
+ if (props.host == null) {
+ if (localInstance.iconUrl == null) {
+ return '/favicon.ico';
+ } else {
+ imageSrc = localInstance.iconUrl;
+ }
+ } else {
+ imageSrc = props.instance?.faviconUrl ?? null;
+ }
+ return getProxiedImageUrlNullable(imageSrc);
+});
const themeColorStyle = computed(() => {
- const themeColor = props.instance?.themeColor ?? localInstance.themeColor ?? '#777777';
+ const themeColor = (props.host == null ? localInstance.themeColor : props.instance?.themeColor) ?? '#777777';
return {
background: `linear-gradient(90deg, ${themeColor}, ${themeColor}00)`,
};
diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue
index 06183a5747..52d0485743 100644
--- a/packages/frontend/src/components/MkNote.vue
+++ b/packages/frontend/src/components/MkNote.vue
@@ -50,7 +50,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-
+
--
cgit v1.2.3-freya
From 26874df4b6b5d7606b74ab7079f59a63a927a039 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Thu, 23 Jan 2025 14:39:17 +0900
Subject: Update about-misskey.vue
---
packages/frontend/src/pages/about-misskey.vue | 3 +++
1 file changed, 3 insertions(+)
(limited to 'packages/frontend/src')
diff --git a/packages/frontend/src/pages/about-misskey.vue b/packages/frontend/src/pages/about-misskey.vue
index 1a86e34969..762e3d2f64 100644
--- a/packages/frontend/src/pages/about-misskey.vue
+++ b/packages/frontend/src/pages/about-misskey.vue
@@ -275,6 +275,9 @@ const patronsWithIcon = [{
}, {
name: '秋瀬カヲル',
icon: 'https://assets.misskey-hub.net/patrons/0f22aeb866484f4fa51db6721e3f9847.jpg',
+}, {
+ name: '新井 治',
+ icon: 'https://assets.misskey-hub.net/patrons/d160876f20394674a17963a0e609600a.jpg',
}];
const patrons = [
--
cgit v1.2.3-freya
From 1080f19e99ffccdebb09c39035d93d9b561ef0e6 Mon Sep 17 00:00:00 2001
From: piuvas
Date: Fri, 24 Jan 2025 18:37:18 -0300
Subject: generalize current language so we match more broadly on fallback
---
packages/backend/src/server/web/boot.embed.js | 2 +-
packages/backend/src/server/web/boot.js | 2 +-
packages/frontend/src/_dev_boot_.ts | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
(limited to 'packages/frontend/src')
diff --git a/packages/backend/src/server/web/boot.embed.js b/packages/backend/src/server/web/boot.embed.js
index b07dce3ac4..1af1dc545b 100644
--- a/packages/backend/src/server/web/boot.embed.js
+++ b/packages/backend/src/server/web/boot.embed.js
@@ -48,7 +48,7 @@
if (supportedLangs.includes(navigator.language)) {
lang = navigator.language;
} else {
- lang = supportedLangs.find(x => x.split('-')[0] === navigator.language);
+ lang = supportedLangs.find(x => x.split('-')[0] === navigator.language.split('-')[0]);
// Fallback
if (lang == null) lang = 'en-US';
diff --git a/packages/backend/src/server/web/boot.js b/packages/backend/src/server/web/boot.js
index bf83340bde..54750e26e5 100644
--- a/packages/backend/src/server/web/boot.js
+++ b/packages/backend/src/server/web/boot.js
@@ -39,7 +39,7 @@
if (supportedLangs.includes(navigator.language)) {
lang = navigator.language;
} else {
- lang = supportedLangs.find(x => x.split('-')[0] === navigator.language);
+ lang = supportedLangs.find(x => x.split('-')[0] === navigator.language.split('-')[0]);
// Fallback
if (lang == null) lang = 'en-US';
diff --git a/packages/frontend/src/_dev_boot_.ts b/packages/frontend/src/_dev_boot_.ts
index f312765dcf..ebce7e735f 100644
--- a/packages/frontend/src/_dev_boot_.ts
+++ b/packages/frontend/src/_dev_boot_.ts
@@ -25,7 +25,7 @@ async function main() {
if (supportedLangs.includes(navigator.language)) {
lang = navigator.language;
} else {
- lang = supportedLangs.find(x => x.split('-')[0] === navigator.language);
+ lang = supportedLangs.find(x => x.split('-')[0] === navigator.language.split('-')[0]);
// Fallback
if (lang == null) lang = 'en-US';
--
cgit v1.2.3-freya
From 35104d87d5174a080143d3604e50bbef974ab04e Mon Sep 17 00:00:00 2001
From: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Sat, 25 Jan 2025 20:58:39 +0900
Subject: revert(dev): フロントエンド・バックエンドを分離する開発モードを廃止
(#15284)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* Revert "chore: 開発モードでフロントエンドとバックエンドを独立して起動するようにする(再) (#12593)"
This reverts commit b0039f0946b02777ad99ad8c92f6555792aa8996.
* revert dev command
* revert embed dev
* 消しすぎた
* filesをプロキシするように
* fix chromatic ci
* Revert "filesをプロキシするように"
This reverts commit 41be2548ce82ba408588c5f0dee007c97d026e55.
* fix: configのhostnameでサーバーを起動するように
* fix
* lint
* Update Changelog
* fix
---------
Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
---
CHANGELOG.md | 1 +
CONTRIBUTING.md | 19 +---
.../backend/src/server/web/ClientServerService.ts | 7 +-
packages/frontend-embed/package.json | 1 -
packages/frontend-embed/src/index.html | 36 --------
packages/frontend-embed/vite.config.local-dev.ts | 96 --------------------
packages/frontend-embed/vite.config.ts | 6 ++
packages/frontend/package.json | 1 -
packages/frontend/src/_dev_boot_.ts | 90 ------------------
packages/frontend/src/index.html | 37 --------
packages/frontend/src/pages/welcome.entrance.a.vue | 1 +
packages/frontend/vite.config.local-dev.ts | 101 ---------------------
packages/frontend/vite.config.ts | 6 ++
scripts/dev.mjs | 4 +-
14 files changed, 23 insertions(+), 383 deletions(-)
delete mode 100644 packages/frontend-embed/src/index.html
delete mode 100644 packages/frontend-embed/vite.config.local-dev.ts
delete mode 100644 packages/frontend/src/_dev_boot_.ts
delete mode 100644 packages/frontend/src/index.html
delete mode 100644 packages/frontend/vite.config.local-dev.ts
(limited to 'packages/frontend/src')
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5b7ae08215..15578d0da4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@
- 新しい設定項目"fulltextSearch.provider"が追加されました. sqlLike, sqlPgroonga, meilisearchのいずれかを設定出来ます.
- すでにMeilisearchをお使いの場合、 **"fulltextSearch.provider"を"meilisearch"に設定する必要** があります.
- 詳細は #14730 および `.config/example.yml` または `.config/docker_example.yml`の'Fulltext search configuration'をご参照願います.
+- 【開発者向け】従来の開発モードでHMRが機能しない問題が修正されたため、バックエンド・フロントエンド分離型の開発モードが削除されました。開発環境においてconfigの変更が必要となる可能性があります。
### General
- Feat: カスタム絵文字管理画面をリニューアル #10996
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index d721ae60d8..0c63f69cf1 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -197,25 +197,10 @@ pnpm dev
command.
- Server-side source files and automatically builds them if they are modified. Automatically start the server process(es).
-- Vite HMR (just the `vite` command) is available. The behavior may be different from production.
- Service Worker is watched by esbuild.
-- The front end can be viewed by accessing `http://localhost:5173`.
-- The backend listens on the port configured with `port` in .config/default.yml.
-If you have not changed it from the default, it will be "http://localhost:3000".
-If "port" in .config/default.yml is set to something other than 3000, you need to change the proxy settings in packages/frontend/vite.config.local-dev.ts.
-
-### `MK_DEV_PREFER=backend pnpm dev`
-pnpm dev has another mode with `MK_DEV_PREFER=backend`.
-
-```
-MK_DEV_PREFER=backend pnpm dev
-```
-
-- This mode is closer to the production environment than the default mode.
-- Vite runs behind the backend (the backend will proxy Vite at /vite).
+- Vite HMR (just the `vite` command) is available. The behavior may be different from production.
+- Vite runs behind the backend (the backend will proxy Vite at /vite and /embed_vite except for websocket used for HMR).
- You can see Misskey by accessing `http://localhost:3000` (Replace `3000` with the port configured with `port` in .config/default.yml).
-- To change the port of Vite, specify with `VITE_PORT` environment variable.
-- HMR may not work in some environments such as Windows.
## Testing
You can run non-backend tests by executing following commands:
diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts
index 1a75096c4e..d450c3fb01 100644
--- a/packages/backend/src/server/web/ClientServerService.ts
+++ b/packages/backend/src/server/web/ClientServerService.ts
@@ -317,16 +317,19 @@ export class ClientServerService {
done();
});
} else {
+ const configUrl = new URL(this.config.url);
+ const urlOriginWithoutPort = configUrl.origin.replace(/:\d+$/, '');
+
const port = (process.env.VITE_PORT ?? '5173');
fastify.register(fastifyProxy, {
- upstream: 'http://localhost:' + port,
+ upstream: urlOriginWithoutPort + ':' + port,
prefix: '/vite',
rewritePrefix: '/vite',
});
const embedPort = (process.env.EMBED_VITE_PORT ?? '5174');
fastify.register(fastifyProxy, {
- upstream: 'http://localhost:' + embedPort,
+ upstream: urlOriginWithoutPort + ':' + embedPort,
prefix: '/embed_vite',
rewritePrefix: '/embed_vite',
});
diff --git a/packages/frontend-embed/package.json b/packages/frontend-embed/package.json
index 3d04c566b6..a0353b4c7f 100644
--- a/packages/frontend-embed/package.json
+++ b/packages/frontend-embed/package.json
@@ -4,7 +4,6 @@
"type": "module",
"scripts": {
"watch": "vite",
- "dev": "vite --config vite.config.local-dev.ts --debug hmr",
"build": "vite build",
"typecheck": "vue-tsc --noEmit",
"eslint": "eslint --quiet \"src/**/*.{ts,vue}\"",
diff --git a/packages/frontend-embed/src/index.html b/packages/frontend-embed/src/index.html
deleted file mode 100644
index 47b0b0e84e..0000000000
--- a/packages/frontend-embed/src/index.html
+++ /dev/null
@@ -1,36 +0,0 @@
-
-
-
-
-
-
-
-
- [DEV] Loading...
-
-
-
-
-
-
-
-
-
-
-
diff --git a/packages/frontend-embed/vite.config.local-dev.ts b/packages/frontend-embed/vite.config.local-dev.ts
deleted file mode 100644
index bf2f478887..0000000000
--- a/packages/frontend-embed/vite.config.local-dev.ts
+++ /dev/null
@@ -1,96 +0,0 @@
-import dns from 'dns';
-import { readFile } from 'node:fs/promises';
-import type { IncomingMessage } from 'node:http';
-import { defineConfig } from 'vite';
-import type { UserConfig } from 'vite';
-import * as yaml from 'js-yaml';
-import locales from '../../locales/index.js';
-import { getConfig } from './vite.config.js';
-
-dns.setDefaultResultOrder('ipv4first');
-
-const defaultConfig = getConfig();
-
-const { port } = yaml.load(await readFile('../../.config/default.yml', 'utf-8'));
-
-const httpUrl = `http://localhost:${port}/`;
-const websocketUrl = `ws://localhost:${port}/`;
-
-// activitypubリクエストはProxyを通し、それ以外はViteの開発サーバーを返す
-function varyHandler(req: IncomingMessage) {
- if (req.headers.accept?.includes('application/activity+json')) {
- return null;
- }
- return '/index.html';
-}
-
-const devConfig: UserConfig = {
- // 基本の設定は vite.config.js から引き継ぐ
- ...defaultConfig,
- root: 'src',
- publicDir: '../assets',
- base: '/embed',
- server: {
- host: 'localhost',
- port: 5174,
- proxy: {
- '/api': {
- changeOrigin: true,
- target: httpUrl,
- },
- '/assets': httpUrl,
- '/static-assets': httpUrl,
- '/client-assets': httpUrl,
- '/files': httpUrl,
- '/twemoji': httpUrl,
- '/fluent-emoji': httpUrl,
- '/sw.js': httpUrl,
- '/streaming': {
- target: websocketUrl,
- ws: true,
- },
- '/favicon.ico': httpUrl,
- '/robots.txt': httpUrl,
- '/embed.js': httpUrl,
- '/identicon': {
- target: httpUrl,
- rewrite(path) {
- return path.replace('@localhost:5173', '');
- },
- },
- '/url': httpUrl,
- '/proxy': httpUrl,
- '/_info_card_': httpUrl,
- '/bios': httpUrl,
- '/cli': httpUrl,
- '/inbox': httpUrl,
- '/emoji/': httpUrl,
- '/notes': {
- target: httpUrl,
- bypass: varyHandler,
- },
- '/users': {
- target: httpUrl,
- bypass: varyHandler,
- },
- '/.well-known': {
- target: httpUrl,
- },
- },
- },
- build: {
- ...defaultConfig.build,
- rollupOptions: {
- ...defaultConfig.build?.rollupOptions,
- input: 'index.html',
- },
- },
-
- define: {
- ...defaultConfig.define,
- _LANGS_FULL_: JSON.stringify(Object.entries(locales)),
- },
-};
-
-export default defineConfig(({ command, mode }) => devConfig);
-
diff --git a/packages/frontend-embed/vite.config.ts b/packages/frontend-embed/vite.config.ts
index 151d316190..3d628c800e 100644
--- a/packages/frontend-embed/vite.config.ts
+++ b/packages/frontend-embed/vite.config.ts
@@ -1,12 +1,17 @@
import path from 'path';
import pluginVue from '@vitejs/plugin-vue';
import { type UserConfig, defineConfig } from 'vite';
+import * as yaml from 'js-yaml';
+import { promises as fsp } from 'fs';
import locales from '../../locales/index.js';
import meta from '../../package.json';
import packageInfo from './package.json' with { type: 'json' };
import pluginJson5 from './vite.json5.js';
+const url = process.env.NODE_ENV === 'development' ? yaml.load(await fsp.readFile('../../.config/default.yml', 'utf-8')).url : null;
+const host = url ? (new URL(url)).hostname : undefined;
+
const extensions = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.json', '.json5', '.svg', '.sass', '.scss', '.css', '.vue'];
/**
@@ -62,6 +67,7 @@ export function getConfig(): UserConfig {
base: '/embed_vite/',
server: {
+ host,
port: 5174,
hmr: {
// バックエンド経由での起動時、Viteは5174経由でアセットを参照していると思い込んでいるが実際は3000から配信される
diff --git a/packages/frontend/package.json b/packages/frontend/package.json
index 8b20980d63..9ec8bb0a83 100644
--- a/packages/frontend/package.json
+++ b/packages/frontend/package.json
@@ -4,7 +4,6 @@
"type": "module",
"scripts": {
"watch": "vite",
- "dev": "vite --config vite.config.local-dev.ts --debug hmr",
"build": "vite build",
"storybook-dev": "nodemon --verbose --watch src --ext \"mdx,ts,vue\" --ignore \"*.stories.ts\" --exec \"pnpm build-storybook-pre && pnpm exec storybook dev -p 6006 --ci\"",
"build-storybook-pre": "(tsc -p .storybook || echo done.) && node .storybook/generate.js && node .storybook/preload-locale.js && node .storybook/preload-theme.js",
diff --git a/packages/frontend/src/_dev_boot_.ts b/packages/frontend/src/_dev_boot_.ts
deleted file mode 100644
index f312765dcf..0000000000
--- a/packages/frontend/src/_dev_boot_.ts
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-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(`--MI_THEME-${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);
-}
diff --git a/packages/frontend/src/index.html b/packages/frontend/src/index.html
deleted file mode 100644
index 84ba9dfabc..0000000000
--- a/packages/frontend/src/index.html
+++ /dev/null
@@ -1,37 +0,0 @@
-
-
-
-
-
-
-
-
- [DEV] Loading...
-
-
-
-
-
-
-
-
-
-
-
diff --git a/packages/frontend/src/pages/welcome.entrance.a.vue b/packages/frontend/src/pages/welcome.entrance.a.vue
index 68938f0bbf..34c5c3ce6c 100644
--- a/packages/frontend/src/pages/welcome.entrance.a.vue
+++ b/packages/frontend/src/pages/welcome.entrance.a.vue
@@ -53,6 +53,7 @@ function getInstanceIcon(instance: Misskey.entities.FederationInstance): string
if (!instance.iconUrl) {
return '';
}
+
return getProxiedImageUrl(instance.iconUrl, 'preview');
}
diff --git a/packages/frontend/vite.config.local-dev.ts b/packages/frontend/vite.config.local-dev.ts
deleted file mode 100644
index 922fb45995..0000000000
--- a/packages/frontend/vite.config.local-dev.ts
+++ /dev/null
@@ -1,101 +0,0 @@
-import dns from 'dns';
-import { readFile } from 'node:fs/promises';
-import type { IncomingMessage } from 'node:http';
-import { defineConfig } from 'vite';
-import type { UserConfig } from 'vite';
-import * as yaml from 'js-yaml';
-import locales from '../../locales/index.js';
-import { getConfig } from './vite.config.js';
-
-dns.setDefaultResultOrder('ipv4first');
-
-const defaultConfig = getConfig();
-
-const { port } = yaml.load(await readFile('../../.config/default.yml', 'utf-8'));
-
-const httpUrl = `http://localhost:${port}/`;
-const websocketUrl = `ws://localhost:${port}/`;
-const embedUrl = `http://localhost:5174/`;
-
-// activitypubリクエストはProxyを通し、それ以外はViteの開発サーバーを返す
-function varyHandler(req: IncomingMessage) {
- if (req.headers.accept?.includes('application/activity+json')) {
- return null;
- }
- return '/index.html';
-}
-
-const devConfig: UserConfig = {
- // 基本の設定は vite.config.js から引き継ぐ
- ...defaultConfig,
- root: 'src',
- publicDir: '../assets',
- base: './',
- server: {
- host: 'localhost',
- port: 5173,
- proxy: {
- '/api': {
- changeOrigin: true,
- target: httpUrl,
- },
- '/assets': httpUrl,
- '/static-assets': httpUrl,
- '/client-assets': httpUrl,
- '/files': httpUrl,
- '/twemoji': httpUrl,
- '/fluent-emoji': httpUrl,
- '/sw.js': httpUrl,
- '/streaming': {
- target: websocketUrl,
- ws: true,
- },
- '/favicon.ico': httpUrl,
- '/robots.txt': httpUrl,
- '/embed.js': httpUrl,
- '/embed': {
- target: embedUrl,
- ws: true,
- },
- '/identicon': {
- target: httpUrl,
- rewrite(path) {
- return path.replace('@localhost:5173', '');
- },
- },
- '/url': httpUrl,
- '/proxy': httpUrl,
- '/_info_card_': httpUrl,
- '/bios': httpUrl,
- '/cli': httpUrl,
- '/inbox': httpUrl,
- '/emoji/': httpUrl,
- '/notes': {
- target: httpUrl,
- bypass: varyHandler,
- },
- '/users': {
- target: httpUrl,
- bypass: varyHandler,
- },
- '/.well-known': {
- target: httpUrl,
- },
- },
- },
- build: {
- ...defaultConfig.build,
- rollupOptions: {
- ...defaultConfig.build?.rollupOptions,
- input: 'index.html',
- },
- },
-
- define: {
- ...defaultConfig.define,
- _LANGS_FULL_: JSON.stringify(Object.entries(locales)),
- },
-};
-
-export default defineConfig(({ command, mode }) => devConfig);
-
diff --git a/packages/frontend/vite.config.ts b/packages/frontend/vite.config.ts
index 3c4b19a571..d1b7c410dc 100644
--- a/packages/frontend/vite.config.ts
+++ b/packages/frontend/vite.config.ts
@@ -2,6 +2,8 @@ import path from 'path';
import pluginReplace from '@rollup/plugin-replace';
import pluginVue from '@vitejs/plugin-vue';
import { type UserConfig, defineConfig } from 'vite';
+import * as yaml from 'js-yaml';
+import { promises as fsp } from 'fs';
import locales from '../../locales/index.js';
import meta from '../../package.json';
@@ -9,6 +11,9 @@ import packageInfo from './package.json' with { type: 'json' };
import pluginUnwindCssModuleClassName from './lib/rollup-plugin-unwind-css-module-class-name.js';
import pluginJson5 from './vite.json5.js';
+const url = process.env.NODE_ENV === 'development' ? yaml.load(await fsp.readFile('../../.config/default.yml', 'utf-8')).url : null;
+const host = url ? (new URL(url)).hostname : undefined;
+
const extensions = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.json', '.json5', '.svg', '.sass', '.scss', '.css', '.vue'];
/**
@@ -64,6 +69,7 @@ export function getConfig(): UserConfig {
base: '/vite/',
server: {
+ host,
port: 5173,
hmr: {
// バックエンド経由での起動時、Viteは5173経由でアセットを参照していると思い込んでいるが実際は3000から配信される
diff --git a/scripts/dev.mjs b/scripts/dev.mjs
index a4c82d46e1..ede77554d2 100644
--- a/scripts/dev.mjs
+++ b/scripts/dev.mjs
@@ -71,13 +71,13 @@ execa('pnpm', ['--filter', 'frontend-shared', 'watch'], {
stderr: process.stderr,
});
-execa('pnpm', ['--filter', 'frontend', process.env.MK_DEV_PREFER === 'backend' ? 'watch' : 'dev'], {
+execa('pnpm', ['--filter', 'frontend', 'watch'], {
cwd: _dirname + '/../',
stdout: process.stdout,
stderr: process.stderr,
});
-execa('pnpm', ['--filter', 'frontend-embed', process.env.MK_DEV_PREFER === 'backend' ? 'watch' : 'dev'], {
+execa('pnpm', ['--filter', 'frontend-embed', 'watch'], {
cwd: _dirname + '/../',
stdout: process.stdout,
stderr: process.stderr,
--
cgit v1.2.3-freya
From 8f37fb6713c1e094acdf995d47741c0447306b41 Mon Sep 17 00:00:00 2001
From: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Sat, 25 Jan 2025 21:01:11 +0900
Subject: fix(frontend): クライアント起動時にURLに #pswp
がある場合は取り除くように (#15339)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* fix(frontend): クライアント起動時にURLに #pswp がある場合は取り除くように
* Update Changelog
---
CHANGELOG.md | 1 +
packages/frontend/src/boot/common.ts | 5 +++++
2 files changed, 6 insertions(+)
(limited to 'packages/frontend/src')
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 15578d0da4..57d8a528f3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -46,6 +46,7 @@
(Cherry-picked from https://github.com/yojo-art/cherrypick/pull/153)
- Fix: 非ログイン時のサーバー概要画面のメニューボタンが押せないことがあるのを修正
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/656)
+- Fix: URLにはじめから`#pswp`が含まれている場合に画像ビューワーがブラウザの戻るボタンで閉じられない問題を修正
### Server
- Enhance: pg_bigmが利用できるよう、ノートの検索をILIKE演算子でなくLIKE演算子でLOWER()をかけたテキストに対して行うように
diff --git a/packages/frontend/src/boot/common.ts b/packages/frontend/src/boot/common.ts
index bfe5c4f5f7..ae6b1aee26 100644
--- a/packages/frontend/src/boot/common.ts
+++ b/packages/frontend/src/boot/common.ts
@@ -98,6 +98,11 @@ export async function common(createVue: () => App) {
// タッチデバイスでCSSの:hoverを機能させる
document.addEventListener('touchend', () => {}, { passive: true });
+ // URLに#pswpを含む場合は取り除く
+ if (location.hash === '#pswp') {
+ history.replaceState(null, '', location.href.replace('#pswp', ''));
+ }
+
// 一斉リロード
reloadChannel.addEventListener('message', path => {
if (path !== null) location.href = path;
--
cgit v1.2.3-freya
From f4bca4708eba50cdef4127c74a37678ac25747c8 Mon Sep 17 00:00:00 2001
From: おさむのひと <46447427+samunohito@users.noreply.github.com>
Date: Sun, 26 Jan 2025 14:59:03 +0900
Subject: feat(frontend): リモート絵文字のインポート時に詳細を確認できるように
(#15344)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* feat(frontend): リモート絵文字のインポート時に詳細を確認できるように
* 追加対応
* MkInput -> MkKeyValue
---
CHANGELOG.md | 1 +
locales/index.d.ts | 4 +
locales/ja-JP.yml | 1 +
.../src/components/MkRemoteEmojiEditDialog.vue | 132 +++++++++++++++++++++
.../pages/admin/custom-emojis-manager.remote.vue | 48 +++++++-
.../frontend/src/pages/custom-emojis-manager.vue | 18 +++
6 files changed, 202 insertions(+), 2 deletions(-)
create mode 100644 packages/frontend/src/components/MkRemoteEmojiEditDialog.vue
(limited to 'packages/frontend/src')
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 57d8a528f3..7339dbd3ff 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -23,6 +23,7 @@
(Based on https://github.com/Otaku-Social/maniakey/pull/14)
- Enhance: AiScriptの拡張API関数において引数の型チェックをより厳格に
- Enhance: クエリパラメータでuiを一時的に変更できるように #15240
+- Enhance: リモート絵文字のインポート時に詳細を確認できるように #15336
- Fix: 画面サイズが変わった際にナビゲーションバーが自動で折りたたまれない問題を修正
- Fix: サーバー情報メニューに区切り線が不足していたのを修正
- Fix: ノートがログインしているユーザーしか見れない場合にログインダイアログを閉じるとその後の動線がなくなる問題を修正
diff --git a/locales/index.d.ts b/locales/index.d.ts
index b98fd5d423..83159337ae 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -10635,6 +10635,10 @@ export interface Locale extends ILocale {
"logNothing": string;
};
"_remote": {
+ /**
+ * 選択行の詳細
+ */
+ "selectionRowDetail": string;
/**
* 選択行をインポート
*/
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 638f2a69c3..42c25a58a6 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -2837,6 +2837,7 @@ _customEmojisManager:
failureLogNothing: "失敗ログはありません。"
logNothing: "ログはありません。"
_remote:
+ selectionRowDetail: "選択行の詳細"
importSelectionRows: "選択行をインポート"
importSelectionRangesRows: "選択範囲の行をインポート"
importEmojisButton: "チェックされた絵文字をインポート"
diff --git a/packages/frontend/src/components/MkRemoteEmojiEditDialog.vue b/packages/frontend/src/components/MkRemoteEmojiEditDialog.vue
new file mode 100644
index 0000000000..873b276b3d
--- /dev/null
+++ b/packages/frontend/src/components/MkRemoteEmojiEditDialog.vue
@@ -0,0 +1,132 @@
+
+
+
+
+ :{{ name }}:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ i18n.ts.id }}
+ {{ name }}
+
+
+ {{ i18n.ts.host }}
+ {{ host }}
+
+
+ {{ i18n.ts.license }}
+ {{ license }}
+
+
+
+
+
+ {{ i18n.ts.import }}
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.remote.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.remote.vue
index 9a9d2990ba..14a3b71e53 100644
--- a/packages/frontend/src/pages/admin/custom-emojis-manager.remote.vue
+++ b/packages/frontend/src/pages/admin/custom-emojis-manager.remote.vue
@@ -34,6 +34,16 @@ SPDX-License-Identifier: AGPL-3.0-only
>
host
+
+ license
+
+
import { computed, onMounted, ref, useCssModule } from 'vue';
import * as Misskey from 'misskey-js';
+import MkRemoteEmojiEditDialog from '@/components/MkRemoteEmojiEditDialog.vue';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
import MkButton from '@/components/MkButton.vue';
@@ -135,7 +146,7 @@ import { deviceKind } from '@/scripts/device-kind.js';
import MkPagingButtons from '@/components/MkPagingButtons.vue';
import MkSortOrderEditor from '@/components/MkSortOrderEditor.vue';
import { SortOrder } from '@/components/MkSortOrderEditor.define.js';
-import { useLoading } from "@/components/hook/useLoading.js";
+import { useLoading } from '@/components/hook/useLoading.js';
type GridItem = {
checked: boolean;
@@ -178,12 +189,37 @@ function setupGrid(): GridSetting {
{ bindTo: 'url', icon: 'ti-icons', type: 'image', editable: false, width: 'auto' },
{ bindTo: 'name', title: 'name', type: 'text', editable: false, width: 'auto' },
{ bindTo: 'host', title: 'host', type: 'text', editable: false, width: 'auto' },
+ { bindTo: 'license', title: 'license', type: 'text', editable: false, width: 200 },
{ bindTo: 'uri', title: 'uri', type: 'text', editable: false, width: 'auto' },
{ bindTo: 'publicUrl', title: 'publicUrl', type: 'text', editable: false, width: 'auto' },
],
cells: {
contextMenuFactory: (col, row, value, context) => {
return [
+ {
+ type: 'button',
+ text: i18n.ts._customEmojisManager._remote.selectionRowDetail,
+ icon: 'ti ti-info-circle',
+ action: async () => {
+ const target = customEmojis.value[row.index];
+ const { dispose } = os.popup(MkRemoteEmojiEditDialog, {
+ emoji: {
+ id: target.id,
+ name: target.name,
+ host: target.host!,
+ license: target.license,
+ url: target.publicUrl,
+ },
+ }, {
+ done: () => {
+ dispose();
+ },
+ closed: () => {
+ dispose();
+ },
+ });
+ },
+ },
{
type: 'button',
text: i18n.ts._customEmojisManager._remote.importSelectionRangesRows,
@@ -207,6 +243,7 @@ const currentPage = ref(0);
const queryName = ref(null);
const queryHost = ref(null);
+const queryLicense = ref(null);
const queryUri = ref(null);
const queryPublicUrl = ref(null);
const previousQuery = ref(undefined);
@@ -229,6 +266,7 @@ async function onSearchRequest() {
function onQueryResetButtonClicked() {
queryName.value = null;
queryHost.value = null;
+ queryLicense.value = null;
queryUri.value = null;
queryPublicUrl.value = null;
}
@@ -306,6 +344,7 @@ async function refreshCustomEmojis() {
const query: Misskey.entities.V2AdminEmojiListRequest['query'] = {
name: emptyStrToUndefined(queryName.value),
host: emptyStrToUndefined(queryHost.value),
+ license: emptyStrToUndefined(queryLicense.value),
uri: emptyStrToUndefined(queryUri.value),
publicUrl: emptyStrToUndefined(queryPublicUrl.value),
hostType: 'remote',
@@ -330,6 +369,7 @@ async function refreshCustomEmojis() {
id: it.id,
url: it.publicUrl,
name: it.name,
+ license: it.license,
host: it.host!,
}));
}
@@ -356,6 +396,10 @@ onMounted(async () => {
grid-column: 2 / 3;
}
+.col3 {
+ grid-column: 3 / 4;
+}
+
.root {
padding: 16px;
}
@@ -366,7 +410,7 @@ onMounted(async () => {
.searchArea {
display: grid;
- grid-template-columns: 1fr 1fr;
+ grid-template-columns: 1fr 1fr 1fr;
gap: 16px;
}
diff --git a/packages/frontend/src/pages/custom-emojis-manager.vue b/packages/frontend/src/pages/custom-emojis-manager.vue
index 789a63eb90..82c6d8df4e 100644
--- a/packages/frontend/src/pages/custom-emojis-manager.vue
+++ b/packages/frontend/src/pages/custom-emojis-manager.vue
@@ -78,6 +78,7 @@ import { computed, defineAsyncComponent, ref, shallowRef } from 'vue';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
import MkPagination from '@/components/MkPagination.vue';
+import MkRemoteEmojiEditDialog from '@/components/MkRemoteEmojiEditDialog.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import FormSplit from '@/components/form/split.vue';
import { selectFile } from '@/scripts/select-file.js';
@@ -159,6 +160,19 @@ const edit = (emoji) => {
});
};
+const detailRemoteEmoji = (emoji) => {
+ const { dispose } = os.popup(MkRemoteEmojiEditDialog, {
+ emoji: emoji,
+ }, {
+ done: () => {
+ dispose();
+ },
+ closed: () => {
+ dispose();
+ },
+ });
+};
+
const importEmoji = (emoji) => {
os.apiWithDialog('admin/emoji/copy', {
emojiId: emoji.id,
@@ -169,6 +183,10 @@ const remoteMenu = (emoji, ev: MouseEvent) => {
os.popupMenu([{
type: 'label',
text: ':' + emoji.name + ':',
+ }, {
+ text: i18n.ts.details,
+ icon: 'ti ti-info-circle',
+ action: () => { detailRemoteEmoji(emoji); },
}, {
text: i18n.ts.import,
icon: 'ti ti-plus',
--
cgit v1.2.3-freya
From 791b4500ec405446785acdc6d10c41acd9d2583c Mon Sep 17 00:00:00 2001
From: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Sun, 26 Jan 2025 15:07:12 +0900
Subject: fix(frontend): 画面を閉じる直前にAudioContextを閉じるように (#15080)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* fix(frontend): 画面を閉じる直前にAudioContextを閉じるように
* Update Changelog
* Update CHANGELOG.md
---------
Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
---
CHANGELOG.md | 1 +
packages/frontend/src/scripts/sound.ts | 4 ++++
2 files changed, 5 insertions(+)
(limited to 'packages/frontend/src')
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8536c1a4ef..59fd861ec4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -38,6 +38,7 @@
(Cherry-picked from https://github.com/TeamNijimiss/misskey/commit/800359623e41a662551d774de15b0437b6849bb4)
- Fix: ノート作成画面でファイルの添付可能個数を超えてもノートボタンが押せていた問題を修正
- Fix: 「アカウントを管理」画面で、ユーザー情報の取得に失敗したアカウント(削除されたアカウントなど)が表示されない問題を修正
+- Fix: MacOSでChrome系ブラウザを使用している場合に、Misskeyを閉じた際に他のタブのオーディオ機能と干渉する問題を修正
- Fix: 言語データのキャッシュ状況によっては、埋め込みウィジェットが正しく起動しない問題を修正
- Fix: 「削除して編集」でノートの引用を解除出来なかった問題を修正( #14476 )
- Fix: RSSウィジェットが正しく表示されない問題を修正
diff --git a/packages/frontend/src/scripts/sound.ts b/packages/frontend/src/scripts/sound.ts
index 05f82fce7d..2008afe045 100644
--- a/packages/frontend/src/scripts/sound.ts
+++ b/packages/frontend/src/scripts/sound.ts
@@ -93,6 +93,10 @@ export async function loadAudio(url: string, options?: { useCache?: boolean; })
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (ctx == null) {
ctx = new AudioContext();
+
+ window.addEventListener('beforeunload', () => {
+ ctx.close();
+ });
}
if (options?.useCache ?? true) {
if (cache.has(url)) {
--
cgit v1.2.3-freya
From 297186e492b20c3e54f8cbfe51ec2d7694ca7068 Mon Sep 17 00:00:00 2001
From: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Sun, 26 Jan 2025 20:10:22 +0900
Subject: enhance(frontend): 絵文字管理画面β(ローカル)のUI・UX改善 (#15349)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* enhance(frontend): 絵文字管理画面β(ローカル)のUI・UX改善
* fix
* :art:
* 表示件数をメニューから変更するように
* 確認ダイアログ
* fix i18n
* needWideArea: trueならwidgetの開閉ボタンを表示しないように
* fix: 検索ウィンドウは一つしか開けないように
---
locales/index.d.ts | 43 +-
locales/ja-JP.yml | 13 +-
.../frontend/src/components/MkSortOrderEditor.vue | 8 +-
packages/frontend/src/components/MkSuperMenu.vue | 2 +-
packages/frontend/src/components/MkTagItem.vue | 4 +-
.../src/components/global/MkPageHeader.vue | 12 +-
.../frontend/src/components/grid/MkDataCell.vue | 35 +-
packages/frontend/src/components/grid/MkGrid.vue | 56 ++-
.../frontend/src/components/grid/MkHeaderCell.vue | 6 +-
packages/frontend/src/components/grid/grid.ts | 5 +
.../src/pages/admin/custom-emojis-manager.impl.ts | 3 +-
.../custom-emojis-manager.local.list.logs.vue | 39 ++
.../custom-emojis-manager.local.list.search.vue | 213 ++++++++++
.../admin/custom-emojis-manager.local.list.vue | 471 ++++++++-------------
.../admin/custom-emojis-manager.local.register.vue | 16 +-
.../pages/admin/custom-emojis-manager.local.vue | 39 +-
.../admin/custom-emojis-manager.logs-folder.vue | 102 -----
.../src/pages/admin/custom-emojis-manager.logs.vue | 88 ++++
.../pages/admin/custom-emojis-manager.remote.vue | 26 +-
.../src/pages/admin/custom-emojis-manager2.vue | 13 +-
packages/frontend/src/pages/admin/index.vue | 11 +-
packages/frontend/src/pages/settings/index.vue | 2 +-
packages/frontend/src/ui/universal.vue | 2 +-
23 files changed, 726 insertions(+), 483 deletions(-)
create mode 100644 packages/frontend/src/pages/admin/custom-emojis-manager.local.list.logs.vue
create mode 100644 packages/frontend/src/pages/admin/custom-emojis-manager.local.list.search.vue
delete mode 100644 packages/frontend/src/pages/admin/custom-emojis-manager.logs-folder.vue
create mode 100644 packages/frontend/src/pages/admin/custom-emojis-manager.logs.vue
(limited to 'packages/frontend/src')
diff --git a/locales/index.d.ts b/locales/index.d.ts
index 83159337ae..a0540fd228 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -10588,7 +10588,7 @@ export interface Locale extends ILocale {
*/
"deleteSelectionRows": string;
/**
- * 選択範囲の行を削除
+ * 選択範囲の値をクリア
*/
"deleteSelectionRanges": string;
/**
@@ -10599,6 +10599,10 @@ export interface Locale extends ILocale {
* 検索条件を詳細に設定します。
*/
"searchSettingCaption": string;
+ /**
+ * 表示件数
+ */
+ "searchLimit": string;
/**
* 並び順
*/
@@ -10611,10 +10615,6 @@ export interface Locale extends ILocale {
* 絵文字更新・削除時のログが表示されます。更新・削除操作を行ったり、ページを遷移・リロードすると消えます。
*/
"registrationLogsCaption": string;
- /**
- * エラー
- */
- "alertEmojisRegisterFailedTitle": string;
/**
* 絵文字の更新・削除に失敗しました。詳細は登録ログをご確認ください。
*/
@@ -10691,21 +10691,30 @@ export interface Locale extends ILocale {
*/
"alertDeleteEmojisNothingDescription": string;
/**
- * 確認
+ * ページを移動しますか?
*/
- "confirmUpdateEmojisTitle": string;
+ "confirmMovePage": string;
/**
- * {count}個の絵文字を更新します。実行しますか?
+ * 表示を変更しますか?
*/
- "confirmUpdateEmojisDescription": ParameterizedString<"count">;
+ "confirmChangeView": string;
/**
- * 確認
+ * {count}個の絵文字を更新します。実行しますか?
*/
- "confirmDeleteEmojisTitle": string;
+ "confirmUpdateEmojisDescription": ParameterizedString<"count">;
/**
* チェックがつけられた{count}個の絵文字を削除します。実行しますか?
*/
"confirmDeleteEmojisDescription": ParameterizedString<"count">;
+ /**
+ * 今までに加えた変更がすべてリセットされます。
+ */
+ "confirmResetDescription": string;
+ /**
+ * このページの絵文字に変更が加えられています。
+ * 保存せずにこのままページを移動すると、このページで加えた変更はすべて破棄されます。
+ */
+ "confirmMovePageDesciption": string;
/**
* 絵文字に設定されたロールで検索
*/
@@ -10744,26 +10753,14 @@ export interface Locale extends ILocale {
* このリンクをクリックしてドライブから選択する
*/
"emojiInputAreaList3": string;
- /**
- * 確認
- */
- "confirmRegisterEmojisTitle": string;
/**
* リストに表示されている絵文字を新たなカスタム絵文字として登録します。よろしいですか?(負荷を避けるため、一度の操作で登録可能な絵文字は{count}件までです)
*/
"confirmRegisterEmojisDescription": ParameterizedString<"count">;
- /**
- * 確認
- */
- "confirmClearEmojisTitle": string;
/**
* 編集内容を破棄し、リストに表示されている絵文字をクリアします。よろしいですか?
*/
"confirmClearEmojisDescription": string;
- /**
- * 確認
- */
- "confirmUploadEmojisTitle": string;
/**
* ドラッグ&ドロップされた{count}個のファイルをドライブにアップロードします。実行しますか?
*/
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 42c25a58a6..a578704434 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -2824,13 +2824,13 @@ _customEmojisManager:
copySelectionRows: "選択行をコピー"
copySelectionRanges: "選択範囲をコピー"
deleteSelectionRows: "選択行を削除"
- deleteSelectionRanges: "選択範囲の行を削除"
+ deleteSelectionRanges: "選択範囲の値をクリア"
searchSettings: "検索設定"
searchSettingCaption: "検索条件を詳細に設定します。"
+ searchLimit: "表示件数"
sortOrder: "並び順"
registrationLogs: "登録ログ"
registrationLogsCaption: "絵文字更新・削除時のログが表示されます。更新・削除操作を行ったり、ページを遷移・リロードすると消えます。"
- alertEmojisRegisterFailedTitle: "エラー"
alertEmojisRegisterFailedDescription: "絵文字の更新・削除に失敗しました。詳細は登録ログをご確認ください。"
_logs:
showSuccessLogSwitch: "成功ログを表示"
@@ -2852,10 +2852,12 @@ _customEmojisManager:
markAsDeleteTargetRanges: "選択範囲の行を削除対象にする"
alertUpdateEmojisNothingDescription: "変更された絵文字はありません。"
alertDeleteEmojisNothingDescription: "削除対象の絵文字はありません。"
- confirmUpdateEmojisTitle: "確認"
+ confirmMovePage: "ページを移動しますか?"
+ confirmChangeView: "表示を変更しますか?"
confirmUpdateEmojisDescription: "{count}個の絵文字を更新します。実行しますか?"
- confirmDeleteEmojisTitle: "確認"
confirmDeleteEmojisDescription: "チェックがつけられた{count}個の絵文字を削除します。実行しますか?"
+ confirmResetDescription: "今までに加えた変更がすべてリセットされます。"
+ confirmMovePageDesciption: "このページの絵文字に変更が加えられています。\n保存せずにこのままページを移動すると、このページで加えた変更はすべて破棄されます。"
dialogSelectRoleTitle: "絵文字に設定されたロールで検索"
_register:
uploadSettingTitle: "アップロード設定"
@@ -2866,11 +2868,8 @@ _customEmojisManager:
emojiInputAreaList1: "この枠に画像ファイルまたはディレクトリをドラッグ&ドロップ"
emojiInputAreaList2: "このリンクをクリックしてPCから選択する"
emojiInputAreaList3: "このリンクをクリックしてドライブから選択する"
- confirmRegisterEmojisTitle: "確認"
confirmRegisterEmojisDescription: "リストに表示されている絵文字を新たなカスタム絵文字として登録します。よろしいですか?(負荷を避けるため、一度の操作で登録可能な絵文字は{count}件までです)"
- confirmClearEmojisTitle: "確認"
confirmClearEmojisDescription: "編集内容を破棄し、リストに表示されている絵文字をクリアします。よろしいですか?"
- confirmUploadEmojisTitle: "確認"
confirmUploadEmojisDescription: "ドラッグ&ドロップされた{count}個のファイルをドライブにアップロードします。実行しますか?"
_embedCodeGen:
diff --git a/packages/frontend/src/components/MkSortOrderEditor.vue b/packages/frontend/src/components/MkSortOrderEditor.vue
index da08f12297..9decacc5f5 100644
--- a/packages/frontend/src/components/MkSortOrderEditor.vue
+++ b/packages/frontend/src/components/MkSortOrderEditor.vue
@@ -12,12 +12,13 @@ SPDX-License-Identifier: AGPL-3.0-only
:iconClass="order.direction === '+' ? 'ti ti-arrow-up' : 'ti ti-arrow-down'"
:exButtonIconClass="'ti ti-x'"
:content="order.key"
+ :class="$style.sortOrderTag"
@click="onToggleSortOrderButtonClicked(order)"
@exButtonClick="onRemoveSortOrderButtonClicked(order)"
/>