parent
dbdb1135bc
commit
65693d0359
@ -2,6 +2,7 @@ import { Plugin } from '@nocobase/client';
|
|||||||
import PluginBlock from './schma-block';
|
import PluginBlock from './schma-block';
|
||||||
import PluginTabSearch from './schma-component/tab-search';
|
import PluginTabSearch from './schma-component/tab-search';
|
||||||
import './assets/svg/index';
|
import './assets/svg/index';
|
||||||
|
import PluginSwiper from './schma-component/swiper';
|
||||||
|
|
||||||
class PluginMobileClient extends Plugin {
|
class PluginMobileClient extends Plugin {
|
||||||
async beforeLoad() {}
|
async beforeLoad() {}
|
||||||
@ -11,6 +12,7 @@ class PluginMobileClient extends Plugin {
|
|||||||
async afterAdd() {
|
async afterAdd() {
|
||||||
this.pm.add(PluginBlock);
|
this.pm.add(PluginBlock);
|
||||||
this.pm.add(PluginTabSearch);
|
this.pm.add(PluginTabSearch);
|
||||||
|
this.pm.add(PluginSwiper);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,21 +1,11 @@
|
|||||||
import { Plugin } from '@nocobase/client';
|
import { Plugin } from '@nocobase/client';
|
||||||
import { NoticeBlock, NoticeBlockInitializer } from './schema-initializer/NoticeBlockInitializer';
|
import { NoticeBlock, NoticeBlockInitializer } from './schema-initializer/NoticeBlockInitializer';
|
||||||
import { SwiperBlock, SwiperBlockInitializer } from './schema-initializer/SwiperBlockInitializer';
|
|
||||||
|
|
||||||
class PluginBlock extends Plugin {
|
class PluginBlock extends Plugin {
|
||||||
async load() {
|
async load() {
|
||||||
this.app.addComponents({
|
this.app.addComponents({
|
||||||
NoticeBlock,
|
NoticeBlock,
|
||||||
NoticeBlockInitializer,
|
NoticeBlockInitializer,
|
||||||
SwiperBlock,
|
|
||||||
SwiperBlockInitializer,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.app.schemaInitializerManager.addItem('MBlockInitializers', 'dataBlocks.swiper', {
|
|
||||||
title: 'swiper',
|
|
||||||
name: 'swiper',
|
|
||||||
type: 'item',
|
|
||||||
Component: 'SwiperBlockInitializer',
|
|
||||||
});
|
});
|
||||||
this.app.schemaInitializerManager.addItem('MBlockInitializers', 'dataBlocks.notice', {
|
this.app.schemaInitializerManager.addItem('MBlockInitializers', 'dataBlocks.notice', {
|
||||||
title: 'notice',
|
title: 'notice',
|
||||||
|
@ -12,6 +12,8 @@ export const createGridCardBlockSchema = (options, componentName) => {
|
|||||||
template,
|
template,
|
||||||
dataSource,
|
dataSource,
|
||||||
settings,
|
settings,
|
||||||
|
value,
|
||||||
|
childrenSettings,
|
||||||
...others
|
...others
|
||||||
} = options;
|
} = options;
|
||||||
const resourceName = resource || association || collection;
|
const resourceName = resource || association || collection;
|
||||||
@ -42,6 +44,13 @@ export const createGridCardBlockSchema = (options, componentName) => {
|
|||||||
[uid()]: {
|
[uid()]: {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
'x-component': componentName,
|
'x-component': componentName,
|
||||||
|
'x-settings': childrenSettings,
|
||||||
|
'x-component-props': {
|
||||||
|
resourceName,
|
||||||
|
fieldName: {
|
||||||
|
value,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -1,71 +0,0 @@
|
|||||||
import {
|
|
||||||
DataBlockInitializer,
|
|
||||||
Icon,
|
|
||||||
css,
|
|
||||||
useCollectionManager,
|
|
||||||
useSchemaInitializer,
|
|
||||||
useSchemaInitializerItem,
|
|
||||||
} from '@nocobase/client';
|
|
||||||
import React from 'react';
|
|
||||||
import { Swiper, Toast } from 'antd-mobile';
|
|
||||||
import { createGridCardBlockSchema } from '../schema-create/createGridCardBlockSchma';
|
|
||||||
import { SwiperIcon } from '../../assets/svg';
|
|
||||||
|
|
||||||
export const SwiperBlock = () => {
|
|
||||||
const colors = ['#ace0ff', '#bcffbd', '#e4fabd', '#ffcfac'];
|
|
||||||
const items = colors.map((color, index) => (
|
|
||||||
<Swiper.Item key={index}>
|
|
||||||
<div
|
|
||||||
className={css`
|
|
||||||
height: 120px;
|
|
||||||
color: #ffffff;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
font-size: 48px;
|
|
||||||
user-select: none;
|
|
||||||
`}
|
|
||||||
style={{ background: color }}
|
|
||||||
onClick={() => {
|
|
||||||
Toast.show(`你点击了卡片 ${index + 1}`);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{index + 1}
|
|
||||||
</div>
|
|
||||||
</Swiper.Item>
|
|
||||||
));
|
|
||||||
return (
|
|
||||||
<Swiper loop autoplay>
|
|
||||||
{items}
|
|
||||||
</Swiper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SwiperBlockInitializer = () => {
|
|
||||||
const { insert } = useSchemaInitializer();
|
|
||||||
const cm = useCollectionManager();
|
|
||||||
const itemConfig = useSchemaInitializerItem();
|
|
||||||
|
|
||||||
const onCreateBlockSchema = async ({ item }) => {
|
|
||||||
const collection = cm.getCollection(item.name);
|
|
||||||
const schema = createGridCardBlockSchema(
|
|
||||||
{
|
|
||||||
collection: item.name,
|
|
||||||
dataSource: item.dataSource,
|
|
||||||
rowKey: collection.filterTargetKey || 'id',
|
|
||||||
settings: 'blockSettings:gridCard',
|
|
||||||
},
|
|
||||||
'SwiperBlock',
|
|
||||||
);
|
|
||||||
insert(schema);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DataBlockInitializer
|
|
||||||
{...itemConfig}
|
|
||||||
icon={<Icon type="swiper-block" />}
|
|
||||||
componentType={'Swiper'}
|
|
||||||
onCreateBlockSchema={onCreateBlockSchema}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
@ -0,0 +1,44 @@
|
|||||||
|
import { BlockItem, css, useDesignable, useRequest, withDynamicSchemaProps } from '@nocobase/client';
|
||||||
|
import { useFieldSchema } from '@nocobase/schema';
|
||||||
|
import { Swiper } from 'antd-mobile';
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
export const SwiperBlock = withDynamicSchemaProps((props) => {
|
||||||
|
const { fieldValue, data, changeNav } = props;
|
||||||
|
if (!data) return;
|
||||||
|
const items = data['data']
|
||||||
|
?.map((imgItem, index) => {
|
||||||
|
return imgItem[fieldValue] ? (
|
||||||
|
<Swiper.Item key={index}>
|
||||||
|
<div
|
||||||
|
className={css`
|
||||||
|
height: 20vh;
|
||||||
|
color: #ffffff;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 48px;
|
||||||
|
user-select: none;
|
||||||
|
`}
|
||||||
|
style={{
|
||||||
|
backgroundImage: `url(${imgItem[fieldValue][0].url})`,
|
||||||
|
backgroundSize: '100% 100%',
|
||||||
|
backgroundAttachment: 'fixed',
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
changeNav(imgItem);
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
</Swiper.Item>
|
||||||
|
) : null;
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
return (
|
||||||
|
<BlockItem>
|
||||||
|
<Swiper loop autoplay>
|
||||||
|
{items}
|
||||||
|
</Swiper>
|
||||||
|
</BlockItem>
|
||||||
|
);
|
||||||
|
});
|
@ -0,0 +1,50 @@
|
|||||||
|
import {
|
||||||
|
DataBlockInitializer,
|
||||||
|
Icon,
|
||||||
|
useCollectionManager,
|
||||||
|
useSchemaInitializer,
|
||||||
|
useSchemaInitializerItem,
|
||||||
|
} from '@nocobase/client';
|
||||||
|
import React from 'react';
|
||||||
|
import { createGridCardBlockSchema } from '../../schma-block/schema-create/createGridCardBlockSchma';
|
||||||
|
import { ISchema } from '@nocobase/schema';
|
||||||
|
|
||||||
|
export const SwiperBlockInitializer = () => {
|
||||||
|
const { insert } = useSchemaInitializer();
|
||||||
|
const cm = useCollectionManager();
|
||||||
|
const itemConfig = useSchemaInitializerItem();
|
||||||
|
|
||||||
|
const onCreateBlockSchema = async ({ item }) => {
|
||||||
|
const collection = cm.getCollection(item.name);
|
||||||
|
const value = collection.fields.find((field) => field.interface === 'attachment');
|
||||||
|
if (!value) return;
|
||||||
|
const schema: ISchema = {
|
||||||
|
type: 'void',
|
||||||
|
name: item.name,
|
||||||
|
title: collection.title,
|
||||||
|
'x-decorator': 'DataBlockProvider',
|
||||||
|
'x-decorator-props': {
|
||||||
|
collection: item.name,
|
||||||
|
dataSource: 'main',
|
||||||
|
},
|
||||||
|
'x-toolbar': 'BlockSchemaToolbar',
|
||||||
|
'x-settings': 'SwiperFieldSettings',
|
||||||
|
'x-component': 'SwiperBlock',
|
||||||
|
'x-use-component-props': 'useSwiperBlockProps',
|
||||||
|
'x-component-props': {
|
||||||
|
resourceName: item.name,
|
||||||
|
fieldValue: value.name,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
insert(schema);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DataBlockInitializer
|
||||||
|
{...itemConfig}
|
||||||
|
icon="swiper-block"
|
||||||
|
componentType={'Swiper'}
|
||||||
|
onCreateBlockSchema={onCreateBlockSchema}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,124 @@
|
|||||||
|
import {
|
||||||
|
SchemaSettings,
|
||||||
|
SchemaSettingsDataScope,
|
||||||
|
useCollection,
|
||||||
|
useCompile,
|
||||||
|
useDesignable,
|
||||||
|
useFormBlockContext,
|
||||||
|
} from '@nocobase/client';
|
||||||
|
import { useField, useFieldSchema } from '@nocobase/schema';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { useTranslation } from '../../locale';
|
||||||
|
|
||||||
|
export const SwiperFieldSettings = new SchemaSettings({
|
||||||
|
name: 'SwiperFieldSettings',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
name: 'setTheDataScope',
|
||||||
|
Component: SchemaSettingsDataScope,
|
||||||
|
useComponentProps() {
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const { form } = useFormBlockContext();
|
||||||
|
const field = useField();
|
||||||
|
const c = useCollection();
|
||||||
|
const { dn } = useDesignable();
|
||||||
|
return {
|
||||||
|
collectionName: c.getOptions().name,
|
||||||
|
defaultFilter: fieldSchema?.['x-component-props']?.params?.filter || {},
|
||||||
|
form: form,
|
||||||
|
onSubmit: ({ filter }) => {
|
||||||
|
_.set(field.componentProps, 'params', {
|
||||||
|
...field.componentProps?.params,
|
||||||
|
filter,
|
||||||
|
});
|
||||||
|
fieldSchema['x-component-props']['params'] = field.componentProps.params;
|
||||||
|
dn.emit('patch', {
|
||||||
|
schema: {
|
||||||
|
['x-uid']: fieldSchema['x-uid'],
|
||||||
|
'x-component-props': fieldSchema['x-component-props'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'setShowField',
|
||||||
|
type: 'select',
|
||||||
|
useComponentProps() {
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const c = useCollection();
|
||||||
|
const compile = useCompile();
|
||||||
|
const { dn } = useDesignable();
|
||||||
|
const options = c.fields
|
||||||
|
.map((field) => {
|
||||||
|
if (field?.interface === 'attachment') {
|
||||||
|
return {
|
||||||
|
label: compile(field.uiSchema?.title) || field.name,
|
||||||
|
value: field.name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
const onChange = (label) => {
|
||||||
|
const schema = {
|
||||||
|
['x-uid']: fieldSchema['x-uid'],
|
||||||
|
};
|
||||||
|
fieldSchema['x-component-props']['fieldValue'] = label;
|
||||||
|
schema['x-component-props'] = fieldSchema['x-component-props'];
|
||||||
|
dn.emit('patch', {
|
||||||
|
schema,
|
||||||
|
});
|
||||||
|
dn.refresh();
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
title: t('SetShowfield'),
|
||||||
|
options,
|
||||||
|
value: fieldSchema['x-component-props'].fieldValue,
|
||||||
|
onChange,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'setFieldCount',
|
||||||
|
type: 'select',
|
||||||
|
useComponentProps() {
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const { dn } = useDesignable();
|
||||||
|
const options = [];
|
||||||
|
for (let i = 1; i <= 20; i++) {
|
||||||
|
options.push({
|
||||||
|
label: i,
|
||||||
|
value: i,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const onChange = (value) => {
|
||||||
|
fieldSchema['x-component-props']['pageSize'] = value;
|
||||||
|
dn.emit('patch', {
|
||||||
|
schema: {
|
||||||
|
['x-uid']: fieldSchema['x-uid'],
|
||||||
|
['x-component-props']: fieldSchema['x-component-props'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
dn.refresh();
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
title: 'setFieldCount',
|
||||||
|
options,
|
||||||
|
value: fieldSchema['x-component-props'].pageSize || 5,
|
||||||
|
onChange,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'divider',
|
||||||
|
type: 'divider',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'delete',
|
||||||
|
type: 'remove',
|
||||||
|
sort: 100,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
@ -0,0 +1,22 @@
|
|||||||
|
import {
|
||||||
|
CollectionRecordProvider,
|
||||||
|
DataBlockProvider,
|
||||||
|
RemoteSchemaComponent,
|
||||||
|
useCollectionRecord,
|
||||||
|
} from '@nocobase/client';
|
||||||
|
import React from 'react';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
export const SwiperPage = () => {
|
||||||
|
const params = useParams();
|
||||||
|
const { name, collection, field, fieldParams } = params;
|
||||||
|
const record = {};
|
||||||
|
record[field] = fieldParams;
|
||||||
|
return (
|
||||||
|
<DataBlockProvider collection={collection} dataSource="main">
|
||||||
|
<CollectionRecordProvider record={record} parentRecord={null}>
|
||||||
|
<RemoteSchemaComponent uid={name} />
|
||||||
|
</CollectionRecordProvider>
|
||||||
|
</DataBlockProvider>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,33 @@
|
|||||||
|
import { Plugin } from '@nocobase/client';
|
||||||
|
import { SwiperBlockInitializer } from './SwiperBlockInitializer';
|
||||||
|
import { SwiperBlock } from './SwiperBlock';
|
||||||
|
import { SwiperFieldSettings } from './SwiperFieldSettings';
|
||||||
|
import { useSwiperBlockProps } from './useSwiperBlockProps';
|
||||||
|
import { SwiperPage } from './SwiperPage';
|
||||||
|
|
||||||
|
class PluginSwiper extends Plugin {
|
||||||
|
async load() {
|
||||||
|
this.app.addScopes({
|
||||||
|
useSwiperBlockProps,
|
||||||
|
});
|
||||||
|
this.app.addComponents({
|
||||||
|
SwiperBlockInitializer,
|
||||||
|
SwiperBlock,
|
||||||
|
SwiperPage,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.app.schemaInitializerManager.addItem('mobilePage:addBlock', 'dataBlocks.swiper', {
|
||||||
|
title: 'swiper',
|
||||||
|
name: 'swiper',
|
||||||
|
type: 'item',
|
||||||
|
Component: 'SwiperBlockInitializer',
|
||||||
|
});
|
||||||
|
this.app.schemaSettingsManager.add(SwiperFieldSettings);
|
||||||
|
this.app.router.add('mobile.swiper.page', {
|
||||||
|
path: '/mobile/:name/image/:collection/:field/:fieldParams',
|
||||||
|
Component: 'SwiperPage',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PluginSwiper;
|
@ -0,0 +1,86 @@
|
|||||||
|
import { useFieldSchema } from '@nocobase/schema';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { useCollection, useDesignable, useRequest } from '@nocobase/client';
|
||||||
|
|
||||||
|
export const useSwiperBlockProps = () => {
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const properties = fieldSchema.properties ? Object.values(fieldSchema.properties) : null;
|
||||||
|
const { insertAdjacent } = useDesignable();
|
||||||
|
const collection = useCollection();
|
||||||
|
const field = collection.targetKey || 'id';
|
||||||
|
const { filter, pageSize, fieldValue, resourceName } = fieldSchema['x-component-props'];
|
||||||
|
const { data, run } = useRequest({
|
||||||
|
resource: resourceName + ':list',
|
||||||
|
params: {
|
||||||
|
filter,
|
||||||
|
pageSize: pageSize || 5,
|
||||||
|
appends: fieldValue ? [fieldValue] : [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
useEffect(() => {
|
||||||
|
run();
|
||||||
|
}, [filter, pageSize, fieldValue]);
|
||||||
|
const changeNav = (imgItem) => {
|
||||||
|
if (imgItem['isIdentical']) {
|
||||||
|
const existence = properties?.find((value) => value['x-component-props']['fixedComponent']);
|
||||||
|
if (existence) {
|
||||||
|
navigate(`/mobile/${existence['x-uid']}/image/${resourceName}/${field}/${imgItem[field]}`);
|
||||||
|
} else {
|
||||||
|
insertAdjacent('beforeEnd', swiperCustomPage(imgItem, 'fixedComponent'), {
|
||||||
|
onSuccess(res) {
|
||||||
|
if (res) {
|
||||||
|
navigate(`/mobile/${res['x-uid']}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const existence = properties?.find((value) => value.name === String(imgItem.id));
|
||||||
|
if (existence) {
|
||||||
|
navigate(`/mobile/${existence['x-uid']}`);
|
||||||
|
} else {
|
||||||
|
insertAdjacent('beforeEnd', swiperCustomPage(imgItem), {
|
||||||
|
onSuccess(res) {
|
||||||
|
if (res) {
|
||||||
|
navigate(`/mobile/${res['x-uid']}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
fieldValue,
|
||||||
|
changeNav,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const swiperCustomPage = (imgItem, fixedComponent?) => {
|
||||||
|
return {
|
||||||
|
type: 'void',
|
||||||
|
title: imgItem.title,
|
||||||
|
name: imgItem.id,
|
||||||
|
'x-component': 'MMenu.Item',
|
||||||
|
'x-component-props': { imgItem, fixedComponent },
|
||||||
|
'x-designer': 'MMenu.Item.Designer',
|
||||||
|
properties: {
|
||||||
|
page: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'MPage',
|
||||||
|
'x-designer': 'MPage.Designer',
|
||||||
|
'x-component-props': {},
|
||||||
|
properties: {
|
||||||
|
grid: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Grid',
|
||||||
|
'x-initializer': 'mobilePage:addBlock',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
@ -16,7 +16,7 @@ const textFieldsItem: SchemaInitializerItemType<any> = {
|
|||||||
const itemComponent = isMobield ? 'TabSearchCollapsibleInputMItem' : 'TabSearchCollapsibleInputItem';
|
const itemComponent = isMobield ? 'TabSearchCollapsibleInputMItem' : 'TabSearchCollapsibleInputItem';
|
||||||
const children = associatedFields
|
const children = associatedFields
|
||||||
.map((field) => {
|
.map((field) => {
|
||||||
if (canBeSearchField(field.interface) && !field['isForeignKey']) {
|
if (canBeSearchField(field.interface) || (canBeOptionalField(field.interface) && !field['isForeignKey'])) {
|
||||||
const label = field.name;
|
const label = field.name;
|
||||||
return createTabSearchItemSchema({ field, itemComponent, label });
|
return createTabSearchItemSchema({ field, itemComponent, label });
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@ const canBeSearchFields = [
|
|||||||
'color',
|
'color',
|
||||||
'icon',
|
'icon',
|
||||||
'formula',
|
'formula',
|
||||||
|
'sequence',
|
||||||
];
|
];
|
||||||
|
|
||||||
export const isTabSearchCollapsibleInputItem = (component: string) =>
|
export const isTabSearchCollapsibleInputItem = (component: string) =>
|
||||||
|
5319
pnpm-lock.yaml
5319
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user