Initial commit

next
mashirozx 2021-07-09 18:28:29 +08:00
commit 747a1c4f66
239 changed files with 18394 additions and 0 deletions

8
.browserslistrc 100644
View File

@ -0,0 +1,8 @@
> 1%
last 2 versions
not IE 11
Android >= 6
Firefox >= 69
Chrome >= 64
iOS >= 9
not dead

18
.editorconfig 100644
View File

@ -0,0 +1,18 @@
root = true
[*]
end_of_line = lf
insert_final_newline = true
[*.{js,ts,json,vue,php}]
charset = utf-8
indent_style = space
indent_size = 2
[/node_modules/**]
charset = unset
end_of_line = unset
insert_final_newline = unset
trim_trailing_whitespace = unset
indent_style = unset
indent_size = unset

44
.eslintrc.js 100644
View File

@ -0,0 +1,44 @@
module.exports = {
env: {
browser: true,
es2021: true,
},
extends: [
'plugin:vue/essential',
'eslint:recommended',
'@vue/typescript/recommended',
'@vue/prettier',
'@vue/prettier/@typescript-eslint',
],
parserOptions: {
ecmaVersion: 12,
parser: '@typescript-eslint/parser',
sourceType: 'module',
},
plugins: ['vue', '@typescript-eslint', 'file-progress', 'formatjs'],
rules: {
'formatjs/no-offset': 'error',
'file-progress/activate': 1,
indent: ['error', 2, { SwitchCase: 1 }],
'linebreak-style': ['error', 'unix'],
'array-element-newline': ['error', { multiline: true, minItems: 3 }],
quotes: ['error', 'single'],
semi: ['error', 'never'],
'@typescript-eslint/no-explicit-any': 0,
'@typescript-eslint/ban-types': 0,
'vue/no-multiple-template-root': 0,
'prettier/prettier': [
'error',
{
endOfLine: 'lf',
},
],
},
globals: {
Pagination: 'readonly',
WPPostAbstract: 'readonly',
Post: 'readonly',
PostListData: 'readonly',
PostStore: 'readonly',
},
}

14
.gitattributes vendored 100644
View File

@ -0,0 +1,14 @@
# Set the default behavior, in case people don't have core.autocrlf set.
* text=auto
# Explicitly declare text files you want to always be normalized and converted
# to native line endings on checkout.
*.c text
*.h text
# Declare files that will always have LF line endings on checkout.
*.sln text eol=lf
# Denote all files that are truly binary and should not be modified.
*.png binary
*.jpg binary

24
.github/dependabot.yml vendored 100644
View File

@ -0,0 +1,24 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
# Maintain dependencies for GitHub Actions
- package-ecosystem: 'github-actions'
directory: '/'
schedule:
interval: 'daily'
# Maintain dependencies for npm
- package-ecosystem: 'npm'
directory: '/'
schedule:
interval: 'daily'
# Maintain dependencies for Composer
- package-ecosystem: 'composer'
directory: '/'
schedule:
interval: 'daily'

14
.gitignore vendored 100644
View File

@ -0,0 +1,14 @@
node_modules
dist
dist-ssr
*.local
# node
yarn-error.log
package-lock.json
composer.phar
# private config
.env.development
# mac
.DS_Store
# jest
coverage

1
.npmrc 100644
View File

@ -0,0 +1 @@
engine-strict=true

5
.postcssrc.js 100644
View File

@ -0,0 +1,5 @@
module.exports = {
plugins: {
autoprefixer: {},
},
}

50
.prettierrc.js 100644
View File

@ -0,0 +1,50 @@
module.exports = {
// 行宽 default:80
printWidth: 100,
// tab 宽度 default:2
tabWidth: 2,
// 使用 tab 键 default:false
useTabs: false,
// 语句行末是否添加分号 default:true
semi: false,
// 是否使用单引号 default:false
singleQuote: true,
// 对象需要引号在加 default:"as-needed"
quoteProps: 'as-needed',
// jsx单引号 default:false
jsxSingleQuote: false,
// 最后一个对象元素加逗号 default:"es5"
trailingComma: 'es5',
// 在对象字面量声明所使用的的花括号后({)和前(})输出空格 default:true
bracketSpacing: true,
// 将 > 多行 JSX 元素放在最后一行的末尾而不是单独放在下一行不适用于自闭元素。default:false
jsxBracketSameLine: false,
// (x) => {} 是否要有小括号 default:"always"
arrowParens: 'always',
// default:0
rangeStart: 0,
// default:Infinity
rangeEnd: Infinity,
// default:false
insertPragma: false,
// default:false
requirePragma: false,
// 不包装 markdown text default:"preserve"
proseWrap: 'never',
// HTML空白敏感性 default:"css"
htmlWhitespaceSensitivity: 'strict',
// 在 *.vue 文件中 Script 和 Style 标签内的代码是否缩进 default:false
vueIndentScriptAndStyle: false,
// 末尾换行符 default:"lf"
endOfLine: 'lf',
// default:"auto"
embeddedLanguageFormatting: 'auto',
overrides: [
{
files: '*.md',
options: {
tabWidth: 2,
},
},
],
}

13
.vscode/extensions.json vendored 100644
View File

@ -0,0 +1,13 @@
{
"recommendations": [
"bmewburn.vscode-intelephense-client",
"dbaeumer.vscode-eslint",
"mhutchie.git-graph",
"donjayamanne.githistory",
"eamodio.gitlens",
"esbenp.prettier-vscode",
"octref.vetur",
"editorconfig.editorconfig",
"neilbrayfield.php-docblocker"
]
}

131
.vscode/settings.json vendored 100644
View File

@ -0,0 +1,131 @@
{
"php-docblocker.returnGap": true,
"php-docblocker.qualifyClassNames": true,
"php-docblocker.author": {
"name": "mashirozx",
"email": "moezhx@outlook.com"
},
"editor.formatOnSave": true,
"emmet.extensionsPath": [
".vscode/"
],
"emmet.triggerExpansionOnTab": true,
"emmet.showExpandedAbbreviation": "always",
"emmet.includeLanguages": {
"vue": "html",
// "twig": "html"
},
"prettier.requireConfig": true,
"prettier.configPath": ".prettierrc.js",
"prettier.semi": true,
"files.associations": {
"*.json": "jsonc",
// "*.html": "twig"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascript|react]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript|react]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[less]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[css]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[php]": {
"editor.defaultFormatter": "bmewburn.vscode-intelephense-client"
},
"[twig]": {
"editor.defaultFormatter": "mblode.twig-language-2"
},
"intelephense.telemetry.enabled": false,
"intelephense.format.enable": true,
"intelephense.completion.triggerParameterHints": true,
"intelephense.completion.insertUseDeclaration": true,
"intelephense.files.maxSize": 3000000,
"intelephense.stubs": [
"apache",
"bcmath",
"bz2",
"calendar",
"com_dotnet",
"Core",
"ctype",
"curl",
"date",
"dba",
"dom",
"enchant",
"exif",
"FFI",
"fileinfo",
"filter",
"fpm",
"ftp",
"gd",
"gettext",
"gmp",
"hash",
"iconv",
"imap",
"intl",
"json",
"ldap",
"libxml",
"mbstring",
"meta",
"mysqli",
"oci8",
"odbc",
"openssl",
"pcntl",
"pcre",
"PDO",
"pdo_ibm",
"pdo_mysql",
"pdo_pgsql",
"pdo_sqlite",
"pgsql",
"Phar",
"posix",
"pspell",
"readline",
"Reflection",
"session",
"shmop",
"SimpleXML",
"snmp",
"soap",
"sockets",
"sodium",
"SPL",
"sqlite3",
"standard",
"superglobals",
"sysvmsg",
"sysvsem",
"sysvshm",
"tidy",
"tokenizer",
"xml",
"xmlreader",
"xmlrpc",
"xmlwriter",
"xsl",
"Zend OPcache",
"zip",
"zlib",
"wordpress"
]
}

11
.vscode/snippets.json vendored 100644
View File

@ -0,0 +1,11 @@
{
"html": {
"snippets": {
"divc": "div[class=${1}]",
"icon": "icon[name=${1}]",
"view": "div[class=${1}]",
"text": "span[class=\"text\"]",
"image": "Image[src=${1} placeholder=${2} :avatar=\"false\" alt=${2} :draggable=\"false\"]"
}
}
}

36
README.md 100644
View File

@ -0,0 +1,36 @@
# Vue 3 + Typescript + Vite
```bash
wget https://getcomposer.org/installer -O composer.phar
php composer.phar
mv composer.phar /usr/local/bin/composer
composer.phar install
curl -sS https://getcomposer.org/installer | php
```
This template should help get you started developing with Vue 3 and Typescript in Vite.
## Recommended IDE Setup
[VSCode](https://code.visualstudio.com/) + [Vetur](https://marketplace.visualstudio.com/items?itemName=octref.vetur). Make sure to enable `vetur.experimental.templateInterpolationService` in settings!
### If Using `<script setup>`
[`<script setup>`](https://github.com/vuejs/rfcs/pull/227) is a feature that is currently in RFC stage. To get proper IDE support for the syntax, use [Volar](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.volar) instead of Vetur (and disable Vetur).
## Type Support For `.vue` Imports in TS
Since TypeScript cannot handle type information for `.vue` imports, they are shimmed to be a generic Vue component type by default. In most cases this is fine if you don't really care about component prop types outside of templates. However, if you wish to get actual prop types in `.vue` imports (for example to get props validation when using manual `h(...)` calls), you can use the following:
### If Using Volar
Run `Volar: Switch TS Plugin on/off` from VSCode command palette.
### If Using Vetur
1. Install and add `@vuedx/typescript-plugin-vue` to the [plugins section](https://www.typescriptlang.org/tsconfig#plugins) in `tsconfig.json`
2. Delete `src/shims-vue.d.ts` as it is no longer needed to provide module info to Typescript
3. Open `src/main.ts` in VSCode
4. Open the VSCode command palette
5. Search and run "Select TypeScript version" -> "Use workspace version"

2
app/.gitignore vendored 100644
View File

@ -0,0 +1,2 @@
vendor
cache

View File

View File

@ -0,0 +1,47 @@
<?php
namespace Sakura\Controllers;
use Sakura\Controllers\UserController;
use Sakura\Controllers\AvatarController;
use Sakura\Lib\Exception;
class AuthorController extends UserController
{
/**
* Get author meta by ID with mutiple fields
*
* @param integer $author_id
* @param array $fields
*
* @return void
*/
public static function get_author_meta_fields(int $author_id, array $fields)
{
$author_meta = [];
foreach ($fields as $field) {
$meta = get_the_author_meta($field, $author_id);
if (isset($meta)) {
$author_meta[$field] = $meta;
} else {
throw new Exception("No such author or field: \$id={$author_id}, \$fields={$field}");
}
}
return $author_meta;
}
/**
* Get usefull meta fileds of author
*
* @param integer $author_id
*
* @return void
*/
public static function get_author_meta(int $author_id)
{
$fields = ['description', 'display_name', 'nickname', 'user_level', 'user_nicename', 'user_status', 'user_url'];
$meta = self::get_author_meta_fields($author_id, $fields);
$meta['avatar'] = AvatarController::get_avatar($author_id);
return $meta;
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace Sakura\Controllers;
use Sakura\Controllers\BaseController;
class AvatarController extends BaseController
{
/**
* Get avatar set of all sizes
*
* @param mixed (int|string) $id_or_email
* @return string
*/
public static function get_avatar($id_or_email)
{
// TODO: use standard 24/48/96
$sizes = [
// 'small' => 24,
// 'normal' => 48,
// 'large' => 96,
'24' => 24,
'48' => 48,
'96' => 96
];
$avatar_array = [];
foreach ($sizes as $key => $value) {
$avatar_array[$key] = get_avatar_url($id_or_email, ['size' => $value, 'default' => 'avatar_default']);
}
return $avatar_array;
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace Sakura\Controllers;
/**
* The controller abstract base
* @since 1.0.0
* @license GPLv3
* @author mashirozx <moezhx@outlook.com>
*/
class BaseController
{
public static $version = SAKURA_VERSION;
public static $text_domain = SAKURA_TEXT_DOMAIN;
/**
* The rest API request parameters
* @since 0.0.1
* @var WP_REST_Request
*/
protected $request;
/**
* Response status code
* @since 0.0.1
* @var WP_REST_Request
*/
protected $code = 200;
protected function request_parser(\WP_REST_Request $request)
{
$this->request = $request;
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace Sakura\Controllers;
use Sakura\Controllers\TermController;
class CategoryController extends TermController
{
public static function get_the_category(int $post_id)
{
return get_the_category($post_id);
}
}

View File

@ -0,0 +1,108 @@
<?php
namespace Sakura\Controllers;
use Sakura\Lib\Exception;
use Sakura\Controllers\BaseController;
use Sakura\Models\CommentModel;
use Sakura\Controllers\AvatarController;
use WP_REST_Request;
use WP_REST_Response;
class CommentController extends BaseController
{
/**
* Set comment ancestor_id from meta data when init
* @return void
*/
public static function init_comment_ancestor_meta()
{
$res = [];
$comments = get_comments();
foreach ($comments as $comment) {
$comment_ID = $comment->comment_ID;
// delete_comment_meta($comment_ID, self::$ancestor_id_meta_key);
$ancestor_id = CommentModel::get_comment_ancestor_meta($comment_ID);
// $ancestor_id = $ancestor_id == false ? false : intval($ancestor_id);
// array_push($res, [$comment_ID, $ancestor_id]);
if ($ancestor_id === 0) {
array_push($res, "[KEEPT] {$comment_ID}:{$ancestor_id} -> {$comment_ID}:{$ancestor_id}");
} elseif (empty($ancestor_id)) {
$meta = CommentModel::set_comment_ancestor_meta($comment_ID);
array_push($res, "[CREATE] {$comment_ID}:{$ancestor_id} -> {$comment_ID}:{$meta}");
} elseif (empty(CommentModel::get_comments([$ancestor_id]))) {
$meta = CommentModel::update_comment_ancestor_meta($comment_ID);
array_push($res, "[UPDATE] {$comment_ID}:{$ancestor_id} -> {$comment_ID}:{$meta}");
} else {
array_push($res, "[KEEPT] {$comment_ID}:{$ancestor_id} -> {$comment_ID}:{$ancestor_id}");
}
}
return $res;
}
/**
* Set comment ancestor_id from meta data when init
* @return void
*/
public static function init_comment_user_agent_info_meta()
{
$res = [];
$comments = get_comments();
foreach ($comments as $comment) {
$comment_ID = $comment->comment_ID;
// delete_comment_meta($comment_ID, self::$ancestor_id_meta_key);
$user_agent_info = CommentModel::get_comment_user_agent_info_meta($comment_ID);
if (empty($user_agent_info)) {
$meta = CommentModel::set_comment_user_agent_info_meta($comment_ID);
array_push($res, ['type' => 'CREATE', 'ID' => $comment_ID, 'ua' => $meta]);
} else {
$meta = CommentModel::update_comment_user_agent_info_meta($comment_ID);
array_push($res, ['type' => 'UPDATE', 'ID' => $comment_ID, 'ua' => $meta]);
}
}
return $res;
}
/**
* Get comment's all children/descendant
*
* @param integer $comment_ID
* @param integer $per_page
* @param integer $page
* @param integer $offset
* @param string $order (string) How to order retrieved comments. Accepts 'ASC', 'DESC'. Default: 'DESC'.
*
* @return array
*/
public static function get_comment_children(int $comment_ID, int $per_page, int $page, int $offset, string $order)
{
return CommentModel::get_comments_with_public_fields([
'meta_key' => CommentModel::$ancestor_id_meta_key,
'meta_value' => $comment_ID,
'number' => $per_page,
'paged' => $page,
'offset' => $offset,
// (string|array) Comment status or array of statuses. To use 'meta_value' or 'meta_value_num', $meta_key must also be defined. To sort by a specific $meta_query clause, use that clause's array key. Accepts 'comment_agent', 'comment_approved', 'comment_author', 'comment_author_email', 'comment_author_IP', 'comment_author_url', 'comment_content', 'comment_date', 'comment_date_gmt', 'comment_ID', 'comment_karma', 'comment_parent', 'comment_post_ID', 'comment_type', 'user_id', 'comment__in', 'meta_value', 'meta_value_num', the value of $meta_key, and the array keys of $meta_query. Also accepts false, an empty array, or 'none' to disable ORDER BY clause. Default: 'comment_date_gmt'.
// TODO: order by 'like'
'orderby' => 'comment_date_gmt',
'order' => $order,
]);
}
public static function get_comment_meta_fields(int $comment_ID)
{
return CommentModel::get_comment_meta_fields($comment_ID);
}
public static function get_comment_plain(int $comment_ID)
{
$comment = CommentModel::get_comment($comment_ID);
return $comment->comment_content;
}
public static function rest_api_comment_content_filter(array $comment)
{
$comment['content']['plain'] = self::get_comment_plain($comment['id']);
return $comment['content'];
}
}

View File

@ -0,0 +1,111 @@
<?php
namespace Sakura\Controllers;
use WP_REST_Response;
use WP_REST_Request;
use WP_Rewrite;
use Sakura\Controllers\MenuController;
use Sakura\Controllers\CommentController;
class InitStateController extends BaseController
{
public function show(WP_REST_Request $request)
{
$this->request_parser($request);
$data = $this->get_initial_state();
$response = new WP_REST_Response($data);
$response->set_status($this->code);
return $response;
}
public function get_initial_state()
{
return array(
'api_base' => get_rest_url(),
'root' => esc_url_raw(rest_url()),
'nonce' => wp_create_nonce('wp_rest'),
'routing' => $this->get_routing(),
'site_info' => $this->get_site_info(),
'menus' => (new MenuController)->get_menus(),
// 'rewrite_rules' => (new \WP_Rewrite())->rewrite_rules(),
'index' => (new WP_Rewrite())->index,
'recaptcha_site_key' => '6LfHEoEbAAAAAI5p_XBlr1WxEvrsOSNQFCQNcT79', // v2 secret key: 6LfHEoEbAAAAAIh0w2I9PCcVoa0j71mO6t7fipsj
// 'recaptcha_site_key' => '6LdKhX8bAAAAAF5HJprXtKvg3nfBJMfgd2o007PN' // v3 secret key: 6LdKhX8bAAAAAA010EXlQ32FWoYD1J2sLb8SaYLR
);
}
public static function get_routing()
{
$routing = array(
'category_base' => get_option('category_base'),
'page_on_front' => null,
'page_for_posts' => null,
'permalink_structure' => get_option('permalink_structure'),
'show_on_front' => get_option('show_on_front'),
'tag_base' => get_option('tag_base'),
'url' => get_bloginfo('url')
);
if ($routing['show_on_front'] === 'page') {
$front_page_id = get_option('page_on_front');
$posts_page_id = get_option('page_for_posts');
if ($front_page_id) {
$front_page = get_post($front_page_id);
$routing['page_on_front'] = $front_page->post_name;
}
if ($posts_page_id) {
$posts_page = get_post($posts_page_id);
$routing['page_for_posts'] = $posts_page->post_name;
}
}
return $routing;
}
public static function get_site_info()
{
return array(
'woordpress_version' => get_bloginfo('version'),
'sakura_version' => self::$version,
'sakura_text_domain' => self::$text_domain,
'description' => get_bloginfo('description'),
'docTitle' => '',
'loading' => false,
'logo' => get_theme_mod('custom_logo'),
'name' => get_bloginfo('name'),
'posts_per_page' => get_option('posts_per_page'),
'url' => get_bloginfo('url')
);
}
// TODO: need auth first
public function init_ancestor_meta_show(WP_REST_Request $request)
{
$this->request_parser($request);
$data = CommentController::init_comment_ancestor_meta();
$response = new WP_REST_Response($data);
$response->set_status($this->code);
return $response;
}
public function init_user_agent_info_meta_show(WP_REST_Request $request)
{
$this->request_parser($request);
$data = CommentController::init_comment_user_agent_info_meta();
$response = new WP_REST_Response($data);
$response->set_status($this->code);
return $response;
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace Sakura\Controllers;
use Sakura\Controllers\BaseController;
use Sakura\Lib\Exception;
class MediaController extends BaseController
{
/**
* Markup for wp_upload_dir, drop sensitive fs info
*
* @return string ie. "http://wp.moezx.cc/wp-content/uploads"
*/
public static function get_upload_baseurl()
{
$dir = wp_upload_dir();
return $dir['baseurl'];
}
/**
* Get attachment metadata by attachment_id, also returns the resources' url
*
* @param integer $attachment_id
* @return mixed (array | false) return false if attachment_id is invalid (in rest API post endpoint, WP will return attachment_id as 0, which means there is no attachment)
*/
public static function get_attachment_metadata(int $attachment_id)
{
$metadata = wp_get_attachment_metadata($attachment_id);
if (!$metadata) {
// throw new Exception("Invalid file name. \$attachment_id=$attachment_id");
return false;
}
$file = $metadata['file'];
$file_name = basename($file);
$subdir = str_replace($file_name, '', $file);
$url_prefix = Self::get_upload_baseurl() . '/' . $subdir;
$metadata['url'] = $url_prefix . $file_name;
foreach ($metadata['sizes'] as $size => $meta) {
$metadata['sizes'][$size]['url'] = $url_prefix . $meta['file'];
}
return $metadata;
}
}

View File

@ -0,0 +1,68 @@
<?php
namespace Sakura\Controllers;
use Sakura\Helpers\CustomMenuMetaFieldsHelper;
class MenuController extends BaseController
{
public function show(\WP_REST_Request $request)
{
$this->request_parser($request);
$location = isset($_GET['location']) ?? $_GET['location'];
$data = !$location ? $this->get_menus() : $this->get_menu($location);
$response = new \WP_REST_Response($data);
$response->set_status($this->code);
return $response;
}
public function get_menu_location(string $location_name)
{
$locations = \get_nav_menu_locations();
return $locations[$location_name];
}
public function get_menu($location)
{
$id = $this->get_menu_location($location);
if (!$id) {
return [];
}
$menu_items = \wp_get_nav_menu_items($id, array());
$fitered_menu_items = array();
foreach ($menu_items as $menu_item) {
$fitered_menu_item = array(
"id" => $menu_item->ID,
'title' => $menu_item->title,
'url' => $menu_item->url,
'type' => $menu_item->type,
'parent' => intval($menu_item->menu_item_parent),
);
foreach (array_keys(CustomMenuMetaFieldsHelper::fileds()) as $name) {
$fitered_menu_item[$name] = get_post_meta($menu_item->ID, "_custom_menu_meta_{$name}", true);
}
array_push($fitered_menu_items, $fitered_menu_item);
}
return $fitered_menu_items;
}
public function get_menus()
{
$menus = array();
// $locations is an array where ([NAME] = MENU_ID);
$locations = get_nav_menu_locations();
foreach (array_keys($locations) as $location) {
$menu = $this->get_menu($location);
$menus[$location] = $menu;
}
return $menus;
}
}

View File

@ -0,0 +1,9 @@
<?php
namespace Sakura\Controllers;
use Sakura\Controllers\PostController;
class PageController extends PostController
{
}

View File

@ -0,0 +1,118 @@
<?php
namespace Sakura\Controllers;
use Sakura\Controllers\BaseController;
use Sakura\Lib\Exception;
use DOMDocument;
class PostController extends BaseController
{
/**
* Get post comment count
*
* @param integer $post_id
* @return number
*/
public static function get_comments_number(int $post_id)
{
return intval(get_comments_number($post_id));
}
public static function get_pagination_info($request)
{
$the_query = new \WP_Query($_GET);
$total_page = $the_query->post_count;
return $the_query;
}
/**
* Get original post expert
*
* @param integer $post_id
* @return string
*/
public static function get_post_excerpt(int $post_id)
{
$post = get_post($post_id);
if ($post) {
// throw new Exception('whoop');
return $post->post_excerpt;
} else {
throw new Exception("No such post \$id={$post_id}!");
}
}
/**
* Get post original Markdown content
* based on https://wordpress.org/plugins/wp-githuber-md/
*
* @param integer $post_id
* @return mixed string | null
*/
public static function get_post_markdown(int $post_id)
{
$post = get_post($post_id);
if ($post) {
// won't check if post_content_filtered is empty, will check it in js
// TODO: global check if markdown available.
if (property_exists($post, 'post_content_filtered')) {
return html_entity_decode($post->post_content_filtered, ENT_QUOTES);
} else {
return null;
}
}
}
/**
* Content filter
*
* @param object $post $post obj in register_rest_field
* @return object $post content modified
*/
public static function rest_api_post_content_filter(array $post)
{
$post['content']['markdown'] = self::get_post_markdown($post['id']);
return $post['content'];
}
public static function rest_api_post_excerpt_filter($post)
{
$post['excerpt']['plain'] = self::get_post_excerpt($post['id']);
return $post['excerpt'];
}
public static function get_post_views($post_id)
{
if (false) {
// if (!function_exists('wp_statistics_pages')) {
// throw new Exception(__('Please install pulgin WP-Statistics (https://wordpress.org/plugins/wp-statistics)', SAKURA_TEXT_DOMAIN));
// } else {
// return intval(wp_statistics_pages('total', 'uri', $post_id));
// }
} else {
$views = get_post_meta($post_id, 'views', true);
if (!$views) {
return 0;
} else {
return intval($views);
}
}
}
public static function get_post_word_count($post_id)
{
$post = get_post($post_id);
if ($post) {
// return $post->post_content;
$doc = new DOMDocument();
$internalErrors = libxml_use_internal_errors(true);
$doc->loadHTML('<?xml encoding="utf-8" ?>' . $post->post_content);
libxml_use_internal_errors($internalErrors);
$string = $doc->textContent;
$string = preg_replace('/\s+/', '', $string);
return strlen($string);
}
return NAN;
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace Sakura\Controllers;
use Sakura\Controllers\TermController;
class TagController extends TermController
{
public static function get_the_tags(int $post_id)
{
return get_the_tags($post_id);
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace Sakura\Controllers;
use Sakura\Controllers\BaseController;
/**
* Base controller for WP_Term
*/
class TermController extends BaseController
{
public static function get_terms(int $post_id)
{
return get_terms($post_id);
}
}

View File

@ -0,0 +1,9 @@
<?php
namespace Sakura\Controllers;
use Sakura\Controllers\BaseController;
class ThemeController extends BaseController
{
}

View File

@ -0,0 +1,39 @@
<?php
namespace Sakura\Controllers;
use Sakura\Controllers\BaseController;
use Sakura\Controllers\AvatarController;
class UserController extends BaseController
{
/**
* Warning: be carefull to use this, use UserController::get_user_public_meta
* instead if not necessary
*
* @param integer $user_id
* @param string $key
* @param boolean $single
*
* @return mixed array | false
*/
public static function get_user_meta(int $user_id, string $key = '', bool $single = false)
{
return get_user_meta($user_id, $key, $single);
}
public static function get_user_public_meta(int $user_id)
{
$white_list = ['nickname', 'first_name', 'last_name', 'description', 'wp_user_level'];
$meta = self::get_user_meta($user_id);
$public_meta = [];
foreach ($white_list as $key) {
$public_meta[$key] = $meta[$key];
}
$public_meta['avatar'] = AvatarController::get_avatar($user_id);
return $public_meta;
}
}

22
app/footer.php 100644
View File

@ -0,0 +1,22 @@
<?php
/**
* The template for displaying the footer
*
* Contains the closing of the #content div and all content after.
*
* @link https://developer.wordpress.org/themes/basics/template-files/#template-partials
*
* @package WordPress
* @subpackage Twenty_Twenty_One
* @since 1.0.0
*/
?>
</div><!-- #app -->
<?php wp_footer(); ?>
</body>
</html>

18
app/functions.php 100644
View File

@ -0,0 +1,18 @@
<?php
// namespace Sakura;
define('SAKURA_VERSION', wp_get_theme()->get('Version'));
define('SAKURA_TEXT_DOMAIN', wp_get_theme()->get('TextDomain'));
// PHP loaders
require_once(__DIR__ . '/loader.php');
new \Sakura\Helpers\SetupHelper();
new \Sakura\Helpers\WhoopsHelper();
new \Sakura\Helpers\ViteRequireHelper();
new \Sakura\Helpers\CustomMenuMetaFieldsHelper();
new \Sakura\Helpers\CommentHelper();
new \Sakura\Routers\ApiRouter();
new \Sakura\Routers\PagesRouter();

40
app/header.php 100644
View File

@ -0,0 +1,40 @@
<?php
/**
* The header.
*
* This is the template that displays all of the <head> section and everything up until main.
*
* @link https://developer.wordpress.org/themes/basics/template-files/#template-partials
*
* @package WordPress
* @subpackage Twenty_Twenty_One
* @since 1.0.0
*/
// namespace Sakura;
// use Sakura\Utils;
// $template = new Helpers\TemplateHelper();
// $params = [
// 'language_attributes' => Utils\echo_interceptor('language_attributes'),
// 'bloginfo' => Utils\echo_interceptor('bloginfo', 'charset'),
// 'wp_head' => Utils\echo_interceptor('wp_head')
// ];
// echo $template->render('header.twig', $params);
?>
<!doctype html>
<html <?php language_attributes(); ?>>
<head>
<meta charset="<?php bloginfo('charset'); ?>" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<?php wp_head(); ?>
</head>
<body>
<div id="app" class="container">
<h1>Vite is loading</h1>
<p>we will render basic content with PHP here in terms of better SEO.</p>

View File

@ -0,0 +1,86 @@
<?php
namespace Sakura\Helpers;
use Sakura\Controllers\CommentController;
use Sakura\Models\CommentModel;
use WP_Comment;
use WP_Theme;
class CommentHelper
{
public function __construct()
{
add_action('comment_post', [$this, 'create_comment_actions'], 1, 3);
add_action('edit_comment', [$this, 'update_comment_actions'], 1, 2);
add_action('delete_comment', [$this, 'delete_comment_actions'], 1, 2);
// TODO: not sure if it's ok to handle a lot of comments? If not ok, provide only the manual method
// add_action('after_switch_theme', [$this, 'after_switch_theme_actions'], 1, 2);
// deprecated, extends class-wp-rest-comments-controller instaed.
// add_filter('rest_allow_anonymous_comments', '__return_true');
}
/**
* Actions after create comment
*
* @param integer $comment_ID
* @param int|string $comment_approved
* @param array $commentdata
*
* @return void
*/
public static function create_comment_actions(int $comment_ID, $comment_approved, array $commentdata)
{
CommentModel::set_comment_ancestor_meta($comment_ID);
}
public static function update_comment_actions(int $comment_ID, array $data)
{
CommentModel::update_comment_ancestor_meta($comment_ID);
}
public static function delete_comment_actions(int $comment_ID, WP_Comment $comment)
{
// upgrade children/descendant comments' ancestor meta (ancestor means the parent/ancestor whose parent_id === 0)
if (CommentModel::get_comment_ancestor_meta($comment_ID) == 0) {
$descendant_comments = get_comments([
'meta_key' => CommentModel::$ancestor_id_meta_key,
'meta_value' => $comment_ID,
]);
$child_comments = get_comments([
'parent' => $comment_ID,
]);
// set children's parent to 0
foreach ($child_comments as $child_comment) {
// https://developer.wordpress.org/reference/functions/wp_insert_comment/
wp_update_comment([
'comment_ID' => $child_comment->comment_ID,
'comment_parent' => 0,
], false);
}
// update ancestor for descendant
foreach ($descendant_comments as $descendant_comment) {
CommentModel::update_comment_ancestor_meta($descendant_comment->comment_ID);
}
}
}
public static function after_switch_theme_actions(string $old_name, WP_Theme $old_theme)
{
CommentController::init_comment_ancestor_meta();
}
public static function filter_rest_allow_anonymous_comments($false, $request)
{
// return [
// 'code' => 123
// ];
throw new \Exception("opps");
// return true;
}
}

View File

@ -0,0 +1,83 @@
<?php
namespace Sakura\Helpers;
use Exception;
/**
* Add custom fields to menu item
* https://www.kathyisawesome.com/add-custom-fields-to-wordpress-menu-items/
*/
class CustomMenuMetaFieldsHelper
{
public static function fileds()
{
return [
'icon' => [
'label' => __('Icon', SAKURA_TEXT_DOMAIN),
'desc' => __('Menu item icon', SAKURA_TEXT_DOMAIN),
],
'class' => [
'label' => __('Class', SAKURA_TEXT_DOMAIN),
'desc' => __('Menu item class class <code>Array&lt;string&gt;</code>', SAKURA_TEXT_DOMAIN),
],
];
}
public static function is_key_in_fields(string $key)
{
return array_key_exists($key, self::fileds());
}
public function __construct()
{
add_action('wp_nav_menu_item_custom_fields', [$this, 'add_fileds'], 10, 2);
add_action('wp_update_nav_menu_item', [$this, 'update_fileds'], 10, 2);
}
public function add_fileds($item_id, $item)
{
$template = new TemplateHelper();
echo $template->load('custom-menu-meta-fields-helper.twig')->renderBlock('id_input', ['item_id' => $item_id,]);
foreach ($this->fileds() as $key => $value) {
wp_nonce_field("custom_menu_meta_{$key}_nonce", "_custom_menu_meta_{$key}_nonce_name");
$custom_menu_meta = get_post_meta($item_id, "_custom_menu_meta_{$key}", true);
$label = $value['label'];
$desc = $value['desc'];
$esc_attr_custom_menu_meta = esc_attr($custom_menu_meta);
echo $template->load('custom-menu-meta-fields-helper.twig')->renderBlock('input_field', [
'item_id' => $item_id,
'key' => $key,
'label' => $label,
'esc_attr_custom_menu_meta' => $esc_attr_custom_menu_meta,
'desc' => $desc
]);
}
}
/**
* admain-ajax filter
*/
public function update_fileds($menu_id, $menu_item_db_id)
{
// throw new Exception("Debug $menu_id");
foreach ($this->fileds() as $key => $value) {
if (!isset($_POST["_custom_menu_meta_{$key}_nonce_name"])) {
// when first add a menu item, this can be empty (undefined)
return $menu_id;
} else if (!wp_verify_nonce($_POST["_custom_menu_meta_{$key}_nonce_name"], "custom_menu_meta_{$key}_nonce")) {
throw new Exception("Invalid `_custom_menu_meta_{$key}_nonce_name` in {$_POST}, \$menu_id={$menu_id}");
return $menu_id;
}
if (isset($_POST["custom_menu_meta_{$key}"][$menu_item_db_id])) {
$sanitized_data = sanitize_text_field($_POST["custom_menu_meta_{$key}"][$menu_item_db_id]);
update_post_meta($menu_item_db_id, "_custom_menu_meta_{$key}", $sanitized_data);
} else {
delete_post_meta($menu_item_db_id, "_custom_menu_meta_{$key}");
}
}
}
}

View File

@ -0,0 +1,88 @@
<?php
namespace Sakura\Helpers;
class SetupHelper
{
public function __construct()
{
add_action('after_setup_theme', [$this, 'setup']);
// Disable WP Admin Bar
add_filter('show_admin_bar', '__return_false');
// TODO: enable this if http?
// add_filter('wp_is_application_passwords_available', '__return_true');
// Allow rest API CORS.
add_action('rest_api_init', [$this, 'wp_rest_allow_all_cors'], 15);
// post excerpt more context
add_filter('excerpt_more', [$this, 'changes_post_excerpt_more']);
// post excerpt length
add_filter('excerpt_length', [$this, 'changes_post_excerpt_length'], 10);
// count post views
add_action('get_header', [$this, 'set_post_views']);
}
public function setup()
{
add_theme_support('title-tag');
add_theme_support('post-thumbnails');
add_theme_support('custom-logo', array(
'height' => 160,
'width' => 160,
));
register_nav_menus($this->menu_locations());
}
public static function menu_locations()
{
return [
'header_menu' => esc_html('Header Menu (show on page header)', SAKURA_TEXT_DOMAIN),
];
}
public static function wp_rest_allow_all_cors()
{
// Remove the default filter.
remove_filter('rest_pre_serve_request', 'rest_send_cors_headers');
// Add a Custom filter.
add_filter('rest_pre_serve_request', function ($value) {
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, GET, OPTIONS, PUT, DELETE');
header('Access-Control-Allow-Credentials: true');
return $value;
});
}
public function changes_post_excerpt_more(string $more)
{
return ' ...';
}
public function changes_post_excerpt_length(int $length)
{
return 120;
}
/**
* Post view times counter
*
* @return void
*/
public function set_post_views()
{
if (is_singular()) {
global $post;
$post_id = intval($post->ID);
if ($post_id) {
$views = (int) get_post_meta($post_id, 'views', true);
if (!update_post_meta($post_id, 'views', ($views + 1))) {
add_post_meta($post_id, 'views', 1, true);
}
}
}
}
}

View File

@ -0,0 +1,58 @@
<?php
namespace Sakura\Helpers;
use Twig\Loader\FilesystemLoader;
use Twig\Environment;
use Twig\TwigFunction;
/**
* Twig engine template loader markup
* @since 0.0.1
* @license MIT
* @author mashirozx <moezhx@outlook.com>
*/
class TemplateHelper
{
public $loader;
public $twig;
public $template;
public function __construct()
{
$this->loader = new FilesystemLoader(array_map(function ($path) {
return __DIR__ . '/' . $path;
}, $this->loader_path_array()));
$this->twig = new Environment($this->loader);
}
public static function loader_path_array()
{
return [
'../views/helpers',
'../views'
];
}
public function load($template_name)
{
$this->template = $this->twig->load($template_name);
return $this->template;
}
public function render(...$params)
{
return $this->twig->render(...$params);
}
public function addFunction($function_names)
{
if (is_array($function_names)) {
foreach ($function_names as $function_name) {
$this->twig->addFunction(new TwigFunction($function_name, $function_name));
}
} elseif (is_string($function_names)) {
$this->twig->addFunction(new TwigFunction($function_names, $function_names));
}
}
}

View File

@ -0,0 +1,87 @@
<?php
namespace Sakura\Helpers;
use Sakura\Controllers;
class ViteRequireHelper
{
// TODO: use a common .env file
// public $development_host = 'http://192.168.28.26:9000';
public $development_host = 'http://127.0.0.1:9000';
function __construct()
{
add_action('wp_enqueue_scripts', [$this, 'enqueue_common_scripts']);
add_action('wp_enqueue_scripts', [$this, 'enqueue_development_scripts']);
// add_action('wp_enqueue_scripts', [$this, 'enqueue_production_scripts']);
// add tag filters
add_filter('script_loader_tag', [$this, 'script_tag_filter'], 10, 3);
add_filter('style_loader_tag', [$this, 'style_tag_filter'], 10, 3);
}
public function enqueue_development_scripts()
{
wp_enqueue_script('[type:module]vite-client', $this->development_host . '/@vite/client', array(), null, false);
wp_enqueue_script('[type:module]dev-main', $this->development_host . '/src/main.ts', array(), null, true);
}
public function enqueue_production_scripts()
{
$assets_base_path = get_template_directory_uri() . '/assets/dist/';
$manifest = $this->get_manifest_file();
// <script type="module" crossorigin src="http://localhost:9000/assets/index.36b06f45.js"></script>
wp_enqueue_script('[type:module]chunk-vendors.js', $assets_base_path . $manifest['index.html']['file'], array(), null, false);
// <link rel="modulepreload" href="http://localhost:9000/assets/vendor.b3a324ba.js">
foreach ($manifest['index.html']['imports'] as $index => $import) {
wp_enqueue_style("[ref:modulepreload]chunk-vendors-{$index}.js", $assets_base_path . $manifest[$import]['file']);
}
// <link rel="stylesheet" href="http://localhost:9000/assets/index.2c78c25a.css">
foreach ($manifest['index.html']['css'] as $index => $path) {
wp_enqueue_style("sakura-chunk-{$index}.css", $assets_base_path . $path);
}
}
public function enqueue_common_scripts()
{
wp_enqueue_style('style.css', get_template_directory_uri() . '/style.css');
wp_enqueue_style('fontawesome-free', 'https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@5.15.3/css/all.min.css');
// TODO: don't use vue.js as handler
wp_enqueue_script('vue.js', 'https://unpkg.com/vue@next', array(), false, true);
wp_localize_script('vue.js', 'InitState', (new Controllers\InitStateController())->get_initial_state());
wp_enqueue_script('recaptcha', 'https://www.recaptcha.net/recaptcha/api.js?render=6LdKhX8bAAAAAF5HJprXtKvg3nfBJMfgd2o007PN', array(), false, true);
}
public function script_tag_filter($tag, $handle, $src)
{
if (preg_match('/^\[([^:]*)\:([^\]]*)\]/', $handle)) {
preg_match('/^\[([^:]*)\:([^\]]*)\]/', $handle, $matches, PREG_OFFSET_CAPTURE);
$template = new TemplateHelper();
$tag = $template->load('vite-require-helper.twig')->renderBlock('script', ['key' => $matches[1][0], 'value' => $matches[2][0], 'src' => esc_url($src)]);
}
return $tag;
}
public function style_tag_filter($tag, $handle, $src)
{
if (preg_match('/^\[([^:]*)\:([^\]]*)\]/', $handle)) {
preg_match('/^\[([^:]*)\:([^\]]*)\]/', $handle, $matches, PREG_OFFSET_CAPTURE);
$template = new TemplateHelper();
$tag = $template->load('vite-require-helper.twig')->renderBlock('style', ['key' => $matches[1][0], 'value' => $matches[2][0], 'href' => esc_url($src)]);
}
return $tag;
}
public function get_manifest_file()
{
$manifest = file_get_contents(__DIR__ . '/../assets/dist/manifest.json');
return json_decode($manifest, true);
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace Sakura\Helpers;
use Whoops\Handler\PrettyPageHandler;
use Whoops\Handler\JsonResponseHandler;
use Whoops\Handler\PlainTextHandler;
use Whoops\Util\Misc;
use Whoops\Run;
class WhoopsHelper
{
public function __construct()
{
if (self::is_debug()) {
$whoops = new Run();
// TODO: not working??
if (wp_is_json_request()) {
$whoops->pushHandler(new JsonResponseHandler);
} elseif (Misc::isCommandLine()) {
$whoops->pushHandler(new PlainTextHandler);
} else {
$whoops->pushHandler(new PrettyPageHandler);
}
$whoops->register();
}
}
/**
* @return bool
*/
public static function is_debug()
{
return defined('WP_DEBUG') && WP_DEBUG;
}
}

30
app/index.php 100644
View File

@ -0,0 +1,30 @@
<?php
/**
* The main template file
*
* This is the most generic template file in a WordPress theme
* and one of the two required files for a theme (the other being style.css).
* It is used to display a page when nothing more specific matches a query.
* E.g., it puts together the home page when no home.php file exists.
*
* @link https://developer.wordpress.org/themes/basics/template-hierarchy/
*
* @package WordPress
* @subpackage Twenty_Twenty_One
* @since 1.0.0
*/
get_header();
if (have_posts()) {
// Load posts loop.
while (have_posts()) {
the_post();
}
// Previous/next page navigation.
}
get_footer();

View File

@ -0,0 +1,141 @@
<?php
namespace Sakura\Lib;
use WP_REST_Comments_Controller;
use WP_Error;
use WP_REST_Request;
class ClassWpRestCommentsController extends WP_REST_Comments_Controller
{
/**
* Checks if a given request has access to create a comment.
*
* wp-includes/rest-api/endpoints/class-wp-rest-comments-controller.php
*
* Source: https://git.io/JcSan
* Modify based on commit 278843f
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has access to create items, error object otherwise.
*/
public function create_item_permissions_check($request)
{
if (!is_user_logged_in()) {
if (get_option('comment_registration')) {
return new WP_Error(
'rest_comment_login_required',
__('Sorry, you must be logged in to comment.'),
array('status' => 401)
);
}
/**
* Filters whether comments can be created via the REST API without authentication.
*
* Enables creating comments for anonymous users.
*
* @since 4.7.0
*
* @param bool $allow_anonymous Whether to allow anonymous comments to
* be created. Default `false`.
* @param WP_REST_Request $request Request used to generate the
* response.
*/
// $allow_anonymous = apply_filters( 'rest_allow_anonymous_comments', false, $request );
$allow_anonymous = true;
if (!$allow_anonymous) {
return new WP_Error(
'rest_comment_login_required',
__('Sorry, you must be logged in to comment.'),
array('status' => 401)
);
}
}
// Limit who can set comment `author`, `author_ip` or `status` to anything other than the default.
if (isset($request['author']) && get_current_user_id() !== $request['author'] && !current_user_can('moderate_comments')) {
return new WP_Error(
'rest_comment_invalid_author',
/* translators: %s: Request parameter. */
sprintf(__("Sorry, you are not allowed to edit '%s' for comments."), 'author'),
array('status' => rest_authorization_required_code())
);
}
if (isset($request['author_ip']) && !current_user_can('moderate_comments')) {
if (empty($_SERVER['REMOTE_ADDR']) || $request['author_ip'] !== $_SERVER['REMOTE_ADDR']) {
return new WP_Error(
'rest_comment_invalid_author_ip',
/* translators: %s: Request parameter. */
sprintf(__("Sorry, you are not allowed to edit '%s' for comments."), 'author_ip'),
array('status' => rest_authorization_required_code())
);
}
}
if (isset($request['status']) && !current_user_can('moderate_comments')) {
return new WP_Error(
'rest_comment_invalid_status',
/* translators: %s: Request parameter. */
sprintf(__("Sorry, you are not allowed to edit '%s' for comments."), 'status'),
array('status' => rest_authorization_required_code())
);
}
if (empty($request['post'])) {
return new WP_Error(
'rest_comment_invalid_post_id',
__('Sorry, you are not allowed to create this comment without a post.'),
array('status' => 403)
);
}
$post = get_post((int) $request['post']);
if (!$post) {
return new WP_Error(
'rest_comment_invalid_post_id',
__('Sorry, you are not allowed to create this comment without a post.'),
array('status' => 403)
);
}
if ('draft' === $post->post_status) {
return new WP_Error(
'rest_comment_draft_post',
__('Sorry, you are not allowed to create a comment on this post.'),
array('status' => 403)
);
}
if ('trash' === $post->post_status) {
return new WP_Error(
'rest_comment_trash_post',
__('Sorry, you are not allowed to create a comment on this post.'),
array('status' => 403)
);
}
if (!$this->check_read_post_permission($post, $request)) {
return new WP_Error(
'rest_cannot_read_post',
__('Sorry, you are not allowed to read the post for this comment.'),
array('status' => rest_authorization_required_code())
);
}
if (!comments_open($post->ID)) {
return new WP_Error(
'rest_comment_closed',
__('Sorry, comments are closed for this item.'),
array('status' => 403)
);
}
return true;
}
}

View File

@ -0,0 +1,15 @@
<?php
// memo: https://learnku.com/articles/5657/laravel-exceptions-exception-and-error-handling
namespace Sakura\Lib;
use Exception as BaseException;
class Exception extends BaseException
{
public function __construct($message, $code = 0)
{
parent::__construct($message, $code);
}
}

35
app/loader.php 100644
View File

@ -0,0 +1,35 @@
<?php
/*------------------------------------*\
Auto loaders
\*------------------------------------*/
// Composer autoload
require_once(__DIR__ . '/vendor/autoload.php');
// Autoload namespace Sakura
spl_autoload_register(function ($class_name) {
$namespaces = explode('\\', $class_name);
$namespaces_length = count($namespaces);
if ($namespaces[0] !== 'Sakura') {
// new Exception("No such class '{$class_name}'");
return;
}
$path = __DIR__;
$index = 1;
foreach ($namespaces as $namespace) {
if ($index === 1) {
$path .= '';
} elseif ($index < $namespaces_length) {
$path .= '/' . strtolower($namespace);
} else {
$path .= '/' . strtolower(preg_replace('%([a-z])([A-Z])%', '\1-\2', $namespace));
}
$index++;
}
// TODO: check if file exists before require
require_once $path . '.php';
});

View File

@ -0,0 +1,9 @@
<?php
namespace Sakura\Models;
class BaseModel
{
public static $version = SAKURA_VERSION;
public static $text_domain = SAKURA_TEXT_DOMAIN;
}

View File

@ -0,0 +1,231 @@
<?php
namespace Sakura\Models;
use Sakura\Lib\Exception;
use Sakura\Models\BaseModel;
use Sakura\Utils\UaParser;
use Sakura\Utils\IpParser;
use Sakura\Controllers\AvatarController;
class CommentModel extends BaseModel
{
public static $ancestor_id_meta_key = 'ancestor_id';
public static $user_agent_info_meta_key = 'user_agent_info';
public static $user_location_meta_key = 'user_location';
public static function get_comments(array $comment_IDs)
{
return get_comments(['comment__in' => $comment_IDs]);
}
public static function get_comment(int $comment_ID)
{
return get_comment($comment_ID);
}
public static function get_comment_childern(int $comment_ID)
{
return get_comments(['parent' => $comment_ID]);
}
public static function get_comments_of_post(int $post_id)
{
return get_comments(['post_id' => $post_id]);
}
public static function get_comment_parent_id(int $comment_ID)
{
$comment = self::get_comments([$comment_ID]);
return intval($comment[0]->comment_parent);
}
public static function get_comment_ancestor_id(int $comment_ID, $target = true)
{
$parent_id = self::get_comment_parent_id($comment_ID);
if ($parent_id > 0) {
return self::get_comment_ancestor_id($parent_id, false);
} else {
return $target ? $parent_id : $comment_ID;
}
}
public static function set_comment_ancestor_meta(int $comment_ID)
{
$ancestor_id = self::get_comment_ancestor_id($comment_ID);
add_comment_meta($comment_ID, self::$ancestor_id_meta_key, $ancestor_id, true);
return $ancestor_id;
}
public static function update_comment_ancestor_meta(int $comment_ID)
{
$ancestor_id = self::get_comment_ancestor_id($comment_ID);
update_comment_meta($comment_ID, self::$ancestor_id_meta_key, $ancestor_id);
return $ancestor_id;
}
/**
* Get ancestor id from meta
*
* @param integer $comment_ID
* @param boolean $display If ture, will throw error if meta not exist. If flase, will return NULL if not exist.
* @return void
*/
public static function get_comment_ancestor_meta(int $comment_ID, $display = false)
{
$ancestor_id = get_comment_meta($comment_ID, self::$ancestor_id_meta_key, true);
if ($ancestor_id == '0') {
return 0;
} elseif (!$ancestor_id) {
if ($display) {
throw new Exception("Please init ancestor_id first");
}
return NULL;
} else {
return intval($ancestor_id);
}
}
public static function set_comment_user_agent_info_meta(int $comment_ID)
{
$user_agent = self::get_comment($comment_ID)->comment_agent;
$parser = new UaParser($user_agent);
$user_agent_info = $parser->get_public_display_content();
add_comment_meta($comment_ID, self::$user_agent_info_meta_key, $user_agent_info, true);
return $user_agent_info;
}
public static function update_comment_user_agent_info_meta(int $comment_ID)
{
$user_agent = self::get_comment($comment_ID)->comment_agent;
$parser = new UaParser($user_agent);
$user_agent_info = $parser->get_public_display_content();
update_comment_meta($comment_ID, self::$user_agent_info_meta_key, $user_agent_info);
return $user_agent_info;
}
/**
* Get user agent info from meta
*
* @param integer $comment_ID
* @param boolean $display If ture, will throw error if meta not exist. If flase, will return [] if not exist.
* @return void
*/
public static function get_comment_user_agent_info_meta(int $comment_ID, $display = false)
{
$user_agent_info = get_comment_meta($comment_ID, self::$user_agent_info_meta_key, false);
if (empty($user_agent_info) && $display) {
throw new Exception("Please init user_agent_info first");
}
return $user_agent_info;
}
public static function set_comment_user_location_meta(int $comment_ID)
{
$comment_author_IP = self::get_comment($comment_ID)->comment_author_IP;
$location = IpParser::get_location($comment_author_IP);
add_comment_meta($comment_ID, self::$user_location_meta_key, $location, true);
return $location;
}
public static function update_comment_user_location_meta(int $comment_ID)
{
$comment_author_IP = self::get_comment($comment_ID)->comment_author_IP;
$location = IpParser::get_location($comment_author_IP);
update_comment_meta($comment_ID, self::$user_location_meta_key, $location);
return $location;
}
/**
* Get user location from meta
*
* @param integer $comment_ID
* @param boolean $display If ture, will throw error if meta not exist. If flase, will return false if not exist.
* @return void
*/
public static function get_comment_user_location_meta(int $comment_ID, $display = false)
{
$location = get_comment_meta($comment_ID, self::$user_location_meta_key, true);
if (empty($location) && $display) {
throw new Exception("Please init user_location first");
}
return $location;
}
/**
* Get comment public display meta
*
* @param array string|array $args Optional. Array or string of arguments. See WP_Comment_Query::__construct() for information on accepted arguments. Default empty.
* @return void
*/
public static function get_comments_with_public_fields(array $args)
{
$comments = get_comments($args);
$output_comments = [];
foreach ($comments as $comment) {
$output_comment = [
'id' => intval($comment->comment_ID),
'post' => intval($comment->comment_post_ID),
'parent' => intval($comment->comment_parent),
'author' => intval($comment->user_id),
'author_name' => $comment->comment_author,
// 'comment_author_email' => $comment->comment_author_email,
'author_url' => $comment->comment_author_url,
// 'comment_author_IP' => $comment->comment_author_IP,
'date' => $comment->comment_date,
'date_gmt' => $comment->comment_date_gmt,
'content' => [
'rendered' => '', // TODO
'plain' => $comment->comment_content
],
'link' => '', // TODO: get_comment_link(),
// 'comment_karma' => $comment->comment_karma,
'status' => $comment->comment_approved === '1' ? 'approved' : '', // TODO
// 'comment_agent' => $comment->comment_agent,
'type' => $comment->comment_type,
'author_avatar_urls' => AvatarController::get_avatar($comment->comment_author_email),
];
$output_comment['meta_fields'] = self::get_comment_meta_fields($comment->comment_ID);
array_push($output_comments, $output_comment);
}
return $output_comments;
}
/**
* Add fields to input array $comment and return it
*
* @param integer $comment_ID
*
* @return void
*/
public static function get_comment_meta_fields(int $comment_ID)
{
$meta = [];
// $ancestor_id = self::get_comment_ancestor_meta($comment_ID);
// if ($ancestor_id === NULL) {
// $ancestor_id = self::set_comment_ancestor_meta($comment_ID);
// }
// $meta['ancestor_id'] = $ancestor_id;
$user_agent_info = self::get_comment_user_agent_info_meta($comment_ID);
if (empty($user_agent_info)) {
$user_agent_info = self::set_comment_user_agent_info_meta($comment_ID);
}
$meta['user_agent_info'] = $user_agent_info;
$user_location = self::get_comment_user_location_meta($comment_ID);
// TODO: not fully tested if '' or false or NULL
// TODO: language option
if ($user_location === '') {
$user_location = self::set_comment_user_location_meta($comment_ID);
}
$meta['user_location'] = $user_location;
return $meta;
}
}

View File

@ -0,0 +1,337 @@
<?php
namespace Sakura\Routers;
use WP_REST_Controller;
use WP_REST_Server;
use Sakura\Controllers\InitStateController;
use Sakura\Controllers\MenuController;
use Sakura\Controllers\PostController;
use Sakura\Controllers\AuthorController;
use Sakura\Controllers\MediaController;
use Sakura\Controllers\CategoryController;
use Sakura\Controllers\TagController;
use Sakura\Controllers\CommentController;
use Sakura\Lib\ClassWpRestCommentsController;
class ApiRouter extends WP_REST_Controller
{
public function __construct()
{
$this->namespace = 'sakura/v1';
$this->register_rest_routes();
$this->register_rest_fields();
}
/**
* Add routers
* @since 1.0.0
* @license MIT
* @author mashirozx <moezhx@outlook.com>
*/
public function register_rest_routes()
{
add_action('rest_api_init', function () {
// theme's initial states
register_rest_route(
$this->namespace,
'/init-state',
[
'methods' => WP_REST_Server::READABLE,
'callback' => [new InitStateController(), 'show'],
'permission_callback' => function () {
return true;
}
]
);
// get menu items
register_rest_route(
$this->namespace,
'/menu',
[
'methods' => WP_REST_Server::READABLE,
'callback' => [new MenuController(), 'show'],
'permission_callback' => function () {
return true;
}
]
);
// initial comment ancestor meta
// TODO: AUTH
register_rest_route(
$this->namespace,
'/init-comments-ancestor-meta',
[
'methods' => WP_REST_Server::READABLE,
'callback' => [new InitStateController(), 'init_ancestor_meta_show'],
'permission_callback' => function () {
return true;
}
]
);
// initial comment ua info meta
// TODO: AUTH
register_rest_route(
$this->namespace,
'/init-comments-user-agent-info-meta',
[
'methods' => WP_REST_Server::READABLE,
'callback' => [new InitStateController(), 'init_user_agent_info_meta_show'],
'permission_callback' => function () {
return true;
}
]
);
$rest_comments_controller = new ClassWpRestCommentsController();
// custom create comment
register_rest_route(
$this->namespace,
'/comments',
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [$rest_comments_controller, 'create_item'],
'permission_callback' => [$rest_comments_controller, 'create_item_permissions_check'],
]
);
// custom edit comment
register_rest_route(
$this->namespace,
'/comments' . '/(?P<id>[\d]+)',
array(
'args' => array(
'id' => array(
'description' => __('Unique identifier for the comment.'),
'type' => 'integer',
),
),
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => [$rest_comments_controller, 'update_item'],
'permission_callback' => [$rest_comments_controller, 'update_item_permissions_check'],
'args' => $this->get_endpoint_args_for_item_schema(WP_REST_Server::EDITABLE),
),
array(
'methods' => WP_REST_Server::DELETABLE,
'callback' => [$rest_comments_controller, 'delete_item'],
'permission_callback' => [$rest_comments_controller, 'delete_item_permissions_check'],
'args' => array(
'force' => array(
'type' => 'boolean',
'default' => false,
'description' => __('Whether to bypass Trash and force deletion.'),
),
'password' => array(
'description' => __('The password for the parent post of the comment (if the post is password protected).'),
'type' => 'string',
),
),
),
'schema' => array($this, 'get_public_item_schema'),
)
);
});
}
/**
* Add fields to existing endpoint
* @since 1.0.0
* @license MIT
* @author mashirozx <moezhx@outlook.com>
*/
public function register_rest_fields()
{
// add fields to /posts & /pages endpoint
add_action('rest_api_init', function () {
$this->register_wp_post_rest_fields(['post', 'page']);
});
// add fields to /comments endpoint
add_action('rest_api_init', function () {
$this->register_wp_comment_rest_fields(['comment']);
});
}
/**
* Common method of adding rest api fields to WP_POST output
*
* @param array $object_type
* @return void
*/
public function register_wp_post_rest_fields(array $object_type)
{
/**
* Add comment_count field to $post
*/
register_rest_field(
$object_type,
'comment_count',
[
'get_callback' => function ($post, $attr, $request, $object_type) {
return PostController::get_comments_number($post['id']);
},
'update_callback' => null,
'schema' => null
]
);
register_rest_field(
$object_type,
'view_count',
[
'get_callback' => function ($post, $attr, $request, $object_type) {
return PostController::get_post_views($post['id']);
},
'update_callback' => null,
'schema' => null
]
);
register_rest_field(
$object_type,
'words_count',
[
'get_callback' => function ($post, $attr, $request, $object_type) {
return PostController::get_post_word_count($post['id']);
},
'update_callback' => null,
'schema' => null
]
);
/**
* Add markdown field to $post['content]
*/
register_rest_field(
$object_type,
'content',
[
'get_callback' => function ($post, $attr, $request, $object_type) {
return PostController::rest_api_post_content_filter($post);
},
'update_callback' => null,
'schema' => null
]
);
/**
* Add plain field to $post['excerpt]
*/
register_rest_field(
$object_type,
'excerpt',
[
'get_callback' => function ($post, $attr, $request, $object_type) {
return PostController::rest_api_post_excerpt_filter($post);
},
'update_callback' => null,
'schema' => null
]
);
register_rest_field(
$object_type,
'author_meta',
[
'get_callback' => function ($post, $attr, $request, $object_type) {
return AuthorController::get_author_meta($post['author']);
},
'update_callback' => null,
'schema' => null
]
);
register_rest_field(
$object_type,
'featured_media_meta',
[
'get_callback' => function ($post, $attr, $request, $object_type) {
return MediaController::get_attachment_metadata($post['featured_media']);
},
'update_callback' => null,
'schema' => null
]
);
register_rest_field(
$object_type,
'categories_meta',
[
'get_callback' => function ($post, $attr, $request, $object_type) {
return CategoryController::get_the_category($post['id']);
},
'update_callback' => null,
'schema' => null
]
);
register_rest_field(
$object_type,
'tags_meta',
[
'get_callback' => function ($post, $attr, $request, $object_type) {
return TagController::get_the_tags($post['id']);
},
'update_callback' => null,
'schema' => null
]
);
// end of public func
}
/**
* Common method of adding rest api fields to WP_COMMENT output
*
* @param array $object_type
* @return void
*/
public static function register_wp_comment_rest_fields(array $object_type)
{
/**
* Add markdown field to $post['content]
*/
register_rest_field(
$object_type,
'content',
[
'get_callback' => function ($comment, $attr, $request, $object_type) {
return CommentController::rest_api_comment_content_filter($comment);
},
'update_callback' => null,
'schema' => null
]
);
// get the custom meta fields
register_rest_field(
$object_type,
'meta_fields',
[
'get_callback' => function ($comment, $attr, $request, $object_type) {
return CommentController::get_comment_meta_fields($comment['id']);
},
'update_callback' => null,
'schema' => null
]
);
// get comment children preview
// register_rest_field(
// $object_type,
// 'children_preview',
// [
// 'get_callback' => function ($comment, $attr, $request, $object_type) {
// return CommentController::get_comment_children($comment['id'], 3, 1, 0, 'DESC');
// },
// 'update_callback' => null,
// 'schema' => null
// ]
// );
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace Sakura\Routers;
class PagesRouter
{
public function __construct()
{
$this->add_rewrite_rules();
}
public function add_rewrite_rules()
{
// This is the front end auth page. Add this rewrite rule
// to avoid 404 message when auth page first load, so the
// response from PHP can be a blank page.
// TODO: blank
add_action('init', function () {
add_rewrite_rule('sakura/auth$', 'index.php', 'top');
});
// TODO: Docker health check route
// add_action('init', function () {
// add_rewrite_rule('health$', 'index.php', 'top');
// });
}
}

View File

@ -0,0 +1,10 @@
<?php
namespace Sakura\Services;
class PostCommentService
{
// public function create_comment(){
// wp_insert_comment()
// }
}

8
app/style.css 100644
View File

@ -0,0 +1,8 @@
/*
Theme Name: sakura-next
Author: Mashiro
Author URI: https://github.com/mashirozx
Description: Next generation Sakura theme.
Version: 0.0.1
Text Domain: sakura-next
*/

View File

@ -0,0 +1,145 @@
<?php
namespace Sakura\Utils;
use Sakura\Lib\Exception;
use GeoIp2\Database\Reader;
use Ritaswc\ZxIPAddress\IPTool;
class IpParser
{
public static $version = SAKURA_VERSION;
public static $text_domain = SAKURA_TEXT_DOMAIN;
public static function get_location(string $ip)
{
try {
// return self::get_ip_location_geoip2_string($ip);
return self::get_ip_location_qqwary_string($ip);
} catch (Exception $e) {
return self::message()['unknown_error'] . " [current: {$ip}]";
}
}
public static function get_ip_location_qqwary_string(string $ip)
{
return preg_replace('/\s+/', ' ', self::get_ip_location_qqwary($ip)['disp']);
}
public static function get_ip_location_geoip2_string(string $ip)
{
// de en es fr ja pt-BR ru zh-CN
$lang = "zh-CN";
if (self::is_private_ip($ip)) {
return [self::message()['private']];
}
$db = self::get_ip_location_geoip2($ip);
$city = self::auto_select_geoip2_lang($lang, $db->city->names);
$subdivisions = self::auto_select_geoip2_lang($lang, $db->subdivisions[0]->names);
$country = self::auto_select_geoip2_lang($lang, $db->country->names);
$array = [$city, $subdivisions, $country];
if ($lang === 'zh-CN' || 'ja') {
$array = array_reverse($array);
}
return join(" ", array_filter($array));
}
private static function auto_select_geoip2_lang(string $default, array $names)
{
return $names[$default] ?? $names['en'] ?? $names['zh-CN'] ?? $names['de'] ?? $names['es'] ?? $names['fr'] ?? $names['ja'] ?? $names['ru'] ?? $names['pt-BR'] ?? '';
}
public static function message()
{
return [
'private' => __("Intranet", self::$text_domain),
'nomatch' => __("Earth", self::$text_domain),
'unknown_error' => __("IP parser handling error.", self::$text_domain),
];
}
// https://github.com/maxmind/GeoIP2-php#readme
public static function get_ip_location_geoip2(string $ip)
{
if (self::is_private_ip($ip)) {
throw new Exception("GeoIP2 doesn't support private IP range. [current: {$ip}]");
}
$reader = new Reader(__DIR__ . '/../cache/GeoLite2-City.mmdb');
$record = $reader->city($ip);
return $record;
}
public static function get_ip_location_qqwary(string $ip)
{
return IPTool::query($ip);
}
static public function is_ip($ip = NULL): bool
{
return filter_var(
$ip,
FILTER_VALIDATE_IP,
FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6
) === $ip ? TRUE : FALSE;
}
static public function is_ipv4($ip = NULL): bool
{
return filter_var(
$ip,
FILTER_VALIDATE_IP,
FILTER_FLAG_IPV4
) === $ip ? TRUE : FALSE;
}
static public function is_ipv6($ip = NULL): bool
{
return filter_var(
$ip,
FILTER_VALIDATE_IP,
FILTER_FLAG_IPV6
) === $ip ? TRUE : FALSE;
}
static public function is_public_ip($ip = NULL): bool
{
return filter_var(
$ip,
FILTER_VALIDATE_IP,
FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE
) === $ip ? TRUE : FALSE;
}
static public function is_public_ipv4($ip = NULL): bool
{
return filter_var(
$ip,
FILTER_VALIDATE_IP,
FILTER_FLAG_IPV4 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE
) === $ip ? TRUE : FALSE;
}
static public function is_public_ipv6($ip = NULL): bool
{
return filter_var(
$ip,
FILTER_VALIDATE_IP,
FILTER_FLAG_IPV6 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE
) === $ip ? TRUE : FALSE;
}
static public function is_private_ip($ip = NULL): bool
{
return self::is_ip($ip) && !self::is_public_ip($ip);
}
static public function is_private_ipv4($ip = NULL): bool
{
return self::is_ipv4($ip) && !self::is_public_ipv4($ip);
}
static public function is_private_ipv6($ip = NULL): bool
{
return self::is_ipv6($ip) && !self::is_public_ipv6($ip);
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace Sakura\Utils;
class Tools
{
public static function echo_interceptor(callable $callback, ...$args)
{
ob_start();
call_user_func($callback, ...$args);
$output = ob_get_contents();
ob_end_clean();
return $output;
}
// public function get_text_from_dom($node, $text) {
// if (!is_null($node->childNodes)) {
// foreach ($node->childNodes as $node) {
// $text = get_text_from_dom($node, $text);
// }
// }
// else {
// return $text . $node->textContent . ' ';
// }
// return $text;
// }
}

View File

@ -0,0 +1,76 @@
<?php
namespace Sakura\Utils;
use WhichBrowser\Parser;
class UaParser
{
private $result;
public function __construct(string $ua)
{
$this->result = new Parser($ua);
}
public function get_browser_name()
{
return $this->result->browser->getName();
}
public function get_browser_version()
{
return $this->result->browser->getVersion();
}
public function get_engine_name()
{
return $this->result->engine->getName();
}
public function get_engine_version()
{
return $this->result->engine->getVersion();
}
public function get_os_name()
{
return $this->result->os->getName();
}
public function get_os_version()
{
return $this->result->os->getVersion();
}
public function get_manufacturer()
{
return $this->result->device->getManufacturer();
}
public function get_model()
{
return $this->result->device->getModel();
}
public function get_device_type()
{
// TODO: does this need any exception handling?
return $this->result->device->type;
}
public function get_public_display_content()
{
return [
'os_name' => $this->get_os_name(),
'os_version' => $this->get_os_version(),
'browser_name' => $this->get_browser_name(),
'browser_version' => $this->get_browser_version(),
'engine_name' => $this->get_engine_name(),
'engine_version' => $this->get_engine_version(),
'manufacturer' => $this->get_manufacturer(),
'model' => $this->get_model(),
'device_type' => $this->get_device_type(),
];
}
}

View File

@ -0,0 +1,5 @@
<head>
<meta charset="{{bloginfo}}"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
{{wp_head}}
</head>

View File

@ -0,0 +1,14 @@
{% block id_input %}
<input type="hidden" class="nav-menu-id" value="{{item_id}}"/>
{% endblock %}
{% block input_field %}
<p class="field-custom-{{key}} description description-wide">
<label for="custom-menu-meta-{{key}}-{{item_id}}">
{{label}}
<br>
<input type="text" name="custom_menu_meta_{{key}}[{{item_id}}]" id="custom-menu-meta-{{key}}-{{item_id}}" class="widefat edit-menu-item-custom-menu-meta-{{key}}" value="{{esc_attr_custom_menu_meta}}"/>
</label>
{{desc}}
</p>
{% endblock %}

View File

@ -0,0 +1,7 @@
{% block style %}
<link {{key}}="{{value}}" href="{{href}}">
{% endblock %}
{% block script %}
<script {{key}}="{{value}}" crossorigin src="{{src}}"></script>
{% endblock %}

28
composer.json 100644
View File

@ -0,0 +1,28 @@
{
"name": "mashirozx/sakura",
"description": "WordPress theme sakura",
"type": "project",
"require": {
"guzzlehttp/guzzle": "^7.3",
"twig/twig": "^3.0",
"filp/whoops": "^2.13",
"geoip2/geoip2": "^2.11",
"whichbrowser/parser": "^2.1",
"ritaswc/zx-ip-address": "^2.0"
},
"license": "GPLv3",
"autoload": {
"psr-4": {
"Sakura\\": "app/"
}
},
"authors": [
{
"name": "mashirozx",
"email": "moezhx@outlook.com"
}
],
"config": {
"vendor-dir": "app/vendor"
}
}

1167
composer.lock generated 100644

File diff suppressed because it is too large Load Diff

7
docs/dev.md 100644
View File

@ -0,0 +1,7 @@
.env.development
```
SSH_KEY_PATH='~/.ssh/id_rsa'
SSH_REMOTE_HOST='root@8.8.8.8'
SSH_REMOTE_WORK_DIR='/var/www/html/wp-contents/themes/sakura-next'
```

1
docs/mdc.md 100644
View File

@ -0,0 +1 @@
Elevation & shadows: <https://material.io/archive/guidelines/material-design/elevation-shadows.html>

3
docs/thanks.md 100644
View File

@ -0,0 +1,3 @@
# Thanks
[pgbross/vue-material-adapter](https://github.com/pgbross/vue-material-adapter/blob/main/packages/button/button.html)

13
index.html 100644
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

13
jest.config.js 100644
View File

@ -0,0 +1,13 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'jsdom',
moduleNameMapper: {
'@/(.*)$': '<rootDir>/src/$1',
// '^vue$': 'vue/dist/vue.esm-bundler.js',
},
transform: {
'^.+\\.vue$': 'vue-jest',
// '^.+\\js$': 'babel-jest',
'^.+\\.(t|j)sx?$': 'ts-jest',
},
}

100
package.json 100644
View File

@ -0,0 +1,100 @@
{
"name": "sakura-next",
"version": "0.0.0",
"license": "MIT",
"scripts": {
"dev": "vite",
"build": "vite build",
"build:strict": "vue-tsc --noEmit && vite build",
"serve": "vite preview",
"test": "jest --coverage",
"lint": "eslint \"src/**/*.{ts,js,json,vue}\"",
"format": "eslint \"src/**/*.{ts,js,json,vue}\" --fix && prettier \"src/**/*.{ts,js,json,vue}\" --write",
"format:lint": "eslint \"src/**/*.{ts,js,json,vue}\" --fix",
"format:prettier": "prettier \"src/**/*.{ts,js,json,vue}\" --write",
"i18n:extract": "formatjs extract \"src/**/*.{ts,tsx,vue}\" --out-file src/locales/defaultMessages.json --ignore \"src/**/@types\"",
"i18n:compile": "formatjs compile src/locales/defaultMessages.json --out-file src/locales/default.json",
"i18n": "yarn i18n:extract && yarn i18n:compile",
"git:commit": "git add . && git commit",
"composer": "node scripts/rsync.mjs --composer-install",
"remote-download:geoip2": "node scripts/rsync.mjs --geoip2",
"local-download:geoip2": "mkdir -p app/cache && curl -o app/cache/GeoLite2-City.mmdb https://raw.githubusercontent.com/P3TERX/GeoLite.mmdb/download/GeoLite2-City.mmdb",
"rsync": "nodemon -e '*' --watch ./app --ignore ./app/vendor scripts/rsync.mjs",
"rsync:composer": "nodemon --watch './composer.json' --watch './composer.lock' scripts/rsync.mjs --composer",
"gen:icon": "node scripts/import-svg-icons.mjs && eslint \"src/components/icon/**/*.{ts,js,json,vue}\" --fix && prettier \"src/components/icon/**/*.{ts,js,json,vue}\" --write",
"mdc": "node scripts/mdc-upgrade.mjs"
},
"dependencies": {
"@formatjs/intl": "^1.13.2",
"@material/button": "^12.0.0-canary.068fd5028.0",
"@material/card": "^12.0.0-canary.068fd5028.0",
"@material/chips": "^12.0.0-canary.068fd5028.0",
"@material/dialog": "^12.0.0-canary.068fd5028.0",
"@material/elevation": "^12.0.0-canary.068fd5028.0",
"@material/list": "^12.0.0-canary.068fd5028.0",
"@material/menu": "^12.0.0-canary.068fd5028.0",
"@material/ripple": "^12.0.0-canary.068fd5028.0",
"@material/textfield": "^12.0.0-canary.068fd5028.0",
"@material/theme": "^12.0.0-canary.068fd5028.0",
"@material/typography": "^12.0.0-canary.068fd5028.0",
"@vueuse/core": "^5.1.2",
"@yzfe/svgicon": "^1.0.1",
"@yzfe/vue3-svgicon": "^1.0.1",
"axios": "^0.21.1",
"camelcase-keys": "^7.0.0",
"crypto-js": "^4.0.0",
"gsap": "^3.7.0",
"highlight.js": "^11.0.1",
"idb": "^6.1.2",
"lodash": "^4.17.21",
"marked": "^2.1.3",
"normalize.css": "^8.0.1",
"sass": "^1.35.1",
"sass-loader": "^12.1.0",
"snakecase-keys": "^4.0.2",
"uuid": "^8.3.2",
"vue": "^3.1.4",
"vue-intl": "^6.0.6",
"vue-router": "^4.0.10"
},
"devDependencies": {
"@formatjs/cli": "^4.2.27",
"@types/crypto-js": "^4.0.1",
"@types/jest": "^26.0.23",
"@types/marked": "^2.0.3",
"@types/uuid": "^8.3.1",
"@typescript-eslint/eslint-plugin": "^4.28.1",
"@typescript-eslint/parser": "^4.28.2",
"@vitejs/plugin-vue": "^1.2.4",
"@vue/compiler-sfc": "^3.1.4",
"@vue/eslint-config-prettier": "^6.0.0",
"@vue/eslint-config-typescript": "^7.0.0",
"@vue/test-utils": "^2.0.0-rc.9",
"autoprefixer": "^10.2.6",
"colors": "^1.4.0",
"dotenv": "^10.0.0",
"eslint": "^7.30.0",
"eslint-plugin-file-progress": "^1.1.0",
"eslint-plugin-formatjs": "^2.17.0",
"eslint-plugin-prettier": "^3.3.1",
"eslint-plugin-vue": "^7.13.0",
"foreman": "^3.0.1",
"jest": "^27.0.6",
"nodemon": "^2.0.9",
"postcss-import": "^14.0.2",
"prettier": "^2.3.2",
"resize-observer-polyfill": "^1.5.1",
"ts-jest": "^27.0.3",
"type-fest": "^1.2.1",
"typescript": "^4.3.5",
"vite": "^2.4.1",
"vite-plugin-svgicon": "^1.0.0-alpha.0",
"vue-jest": "^5.0.0-alpha.10",
"vue-tsc": "^0.2.0"
},
"engines": {
"npm": "please-use-yarn",
"yarn": ">= 1.22.10",
"node": ">= 14.17.1"
}
}

BIN
public/favicon.ico 100644

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -0,0 +1,53 @@
import { readdirSync, writeFileSync } from 'fs'
import camelCase from 'camelcase'
const iconDir = './src/assets/icons/ui'
const targetDir = './src/components/icon/UiIcon.vue'
const template = (importContent, dataContent) => `
<!-- # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -->
<!-- # Generated by scripts/import-svg-icons.js -->
<template>
<svg-icon :data="svg[$props.name]" original></svg-icon>
</template>
<script>
import { defineComponent } from 'vue'
${importContent}
export default defineComponent({
name: 'Icon',
props: {
name: { type: String },
},
setup(){
const svg = {
${dataContent}
}
return {
svg
}
}
})
</script>
`
const importTemplate = (name, camelCaseName) =>
`import ${camelCaseName} from '@/assets/icons/ui/${name}.svg';\n`
const dataTemplate = (name, camelCaseName) => `'${name}':${camelCaseName},\n`
let importContent = '',
dataContent = ''
readdirSync(iconDir).forEach((file) => {
if (/(.*)\.svg$/.test(file)) {
// console.log(file)
const name = file.match(/(.*)\.svg$/)[1]
const camelCaseName = camelCase(name)
importContent += importTemplate(name, camelCaseName)
dataContent += dataTemplate(name, camelCaseName)
} else {
return false
}
})
const vueContent = template(importContent, dataContent)
writeFileSync(targetDir, vueContent)

View File

@ -0,0 +1,23 @@
'use strict'
import { exec } from 'child_process'
import { readFileSync } from 'fs'
const json = JSON.parse(readFileSync('package.json'))
const dependencies = json.dependencies
let deps = ''
Object.keys(dependencies).forEach((key) => {
if (/@material\//.test(key)) deps += ` ${key}@canary `
})
const command = `yarn upgrade ${deps}`
console.log(command)
exec(command, (error, stdout, stderr) => {
if (error) console.error(error)
if (stderr) console.warn(stderr)
if (stdout) console.log(stdout)
})

62
scripts/rsync.mjs 100644
View File

@ -0,0 +1,62 @@
#!/usr/local/bin/node
import { exec } from 'child_process'
import colors from 'colors'
import dotenv from 'dotenv'
const start = Date.now()
dotenv.config({ path: '.env.development' })
const argv = process.argv.slice(2)
const app = `rsync -a -P -e "ssh -i ${process.env.SSH_KEY_PATH}" ./app/ ${process.env.SSH_REMOTE_HOST}:${process.env.SSH_REMOTE_WORK_DIR}/app/ --exclude 'vendor' --exclude 'cache' --delete -ic`
let composer = `rsync -a -P -e "ssh -i ${process.env.SSH_KEY_PATH}" ./composer.* ${process.env.SSH_REMOTE_HOST}:${process.env.SSH_REMOTE_WORK_DIR}/ -ic`
const rmLF = (str) => {
if (str.lastIndexOf('\n') > 0) {
return str.substring(0, str.lastIndexOf('\n'))
} else {
return str
}
}
if (argv.indexOf('--composer-install') >= 0)
composer += ` && ssh -i ${process.env.SSH_KEY_PATH} ${process.env.SSH_REMOTE_HOST} 'cd ${process.env.SSH_REMOTE_WORK_DIR} && COMPOSER_ALLOW_SUPERUSER=1 composer install'`
let command
// TODO: separate these!
if (argv.indexOf('--geoip2') >= 0) {
command = `ssh -i ${process.env.SSH_KEY_PATH} ${process.env.SSH_REMOTE_HOST} 'cd ${process.env.SSH_REMOTE_WORK_DIR} && mkdir -p app/cache && curl -o app/cache/GeoLite2-City.mmdb https://raw.githubusercontent.com/P3TERX/GeoLite.mmdb/download/GeoLite2-City.mmdb'`
} else if (argv.indexOf('--qqwry') >= 0) {
command = `ssh -i ${process.env.SSH_KEY_PATH} ${process.env.SSH_REMOTE_HOST} 'cd ${process.env.SSH_REMOTE_WORK_DIR} && mkdir -p app/cache && curl -o app/cache/qqwry.dat https://raw.githubusercontent.com/out0fmemory/qqwry.dat/master/qqwry_lastest.dat'`
} else if (argv.indexOf('--composer') < 0 && argv.indexOf('--composer-install') < 0) {
command = app
} else {
command = composer
}
// console.log(command)
exec(command, (error, stdout, stderr) => {
if (stdout) stdout = stdout.replace('sending incremental file list\n', '')
if (error) {
console.log(rmLF(error.message))
console.log(
colors.inverse(colors.magenta(' rsync error '), ` ready in ${Date.now() - start}ms `)
)
return
}
if (stderr) {
console.log(rmLF(stderr))
console.log(
colors.inverse(colors.magenta(' rsync stderr '), ` ready in ${Date.now() - start}ms `)
)
return
}
if (stdout.trim())
// --itemize-changes: https://www.samba.org/ftp/rsync/rsync.html
console.log(colors.inverse(colors.green('YXcstogax File '), 'See: https://git.io/JnrAP '))
console.log(stdout.trim() ? rmLF(stdout) : colors.green('Already up to date, nothing to sync.'))
console.log(colors.inverse(colors.green(' rsync done '), ` ready in ${Date.now() - start} ms `))
})

119
src/@types/declarations.d.ts vendored 100644
View File

@ -0,0 +1,119 @@
// declare module 'decamelize-keys' {
// export default function decamelizeKeys<T>(input: T, separator: string): T
// }
/**
* Sakura initState
*/
declare var InitState: any
/**
* reCaptcha
*/
declare var grecaptcha: any
interface Pagination {
page: number
perPage: number
totalPage: number
totalCount: number
}
interface WPPostAbstract {
id: number
date: string
modified: string
slug: string
status: string
type: string
link: string
title: {
rendered: string
}
content: {
rendered: string
protected: boolean
markdown?: string | null
}
excerpt: {
rendered: string
protected: boolean
plain: string
}
author: number
authorMeta: object
featuredMedia: number
featuredMediaMeta: { [key: string]: any }
commentStatus: string
pingStatus: string
sticky: boolean
template: string
format: string
meta: [any?]
categories: [number?]
categoriesMeta: { [key: string]: any }
tags: [number?]
tagsMeta: { [key: string]: any }
commentCount: number
viewCount: number
wordsCount: number
links: { [key: string]: any }
}
interface Post extends WPPostAbstract {
[key: string]: any
}
interface PostListData {
[key: number]: Post
}
interface PostStore {
data: PostListData // where we save all post data (indexed by id)
list: {
// the saved list type, ie. 'homepage', 'catName'
[namespace: string]: {
idList: number[] // where we save post lists (only ID order)
pagination: Pagination
defaultOrder: number[]
}
}
}
interface WPCommentAbstract {
id: number // view, edit, embed
author: number // view, edit, embed
authorEmail?: string // edit
authorIp?: string // edit
authorName: string // view, edit, embed
authorUrl: string // view, edit, embed
authorUserAgent?: string // edit
content: {
rendered: string
markdown?: string | null
} // view, edit, embed
date: string // view, edit, embed
dateGmt?: string // view, edit
link: string // view, edit, embed
parent: number // view, edit, embed
post?: number // view, edit
status?: string // view, edit
type: string // view, edit, embed
authorAvatarUrls: { [key: string]: any } // view, edit, embed
meta?: { [key: string]: any } // view, edit
}
interface Comment extends WPCommentAbstract {
ancestor?: number
metaFields: {
userAgentInfo: string
userLocation: string
}
}
interface CommentStore {
[namespace: string]: {
paged: { [page: number]: Comment[] }
pagination: Pagination
}
}

5
src/@types/shims-vue.d.ts vendored 100644
View File

@ -0,0 +1,5 @@
declare module '*.vue' {
import { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}

26
src/App.vue 100644
View File

@ -0,0 +1,26 @@
<template>
<router-view v-slot="{ Component }">
<keep-alive>
<component :is="Component" :key="$route.fullPath"></component>
</keep-alive>
</router-view>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { init } from '@/store'
import { useInjector } from '@/hooks'
export default defineComponent({
name: 'App',
setup() {
const { fetchWpJson } = useInjector(init)
fetchWpJson()
},
})
</script>
<style lang="scss">
@import 'normalize.css/normalize.css';
@import '@/styles/index.scss';
</style>

View File

@ -0,0 +1,11 @@
import request from '@/utils/http'
export default {
postJwtAuthToken({ username, password }: { username: string; password: string }): Promise<any> {
return request({
url: '/jwt-auth/v1/token',
method: 'POST',
data: { username, password },
})
},
}

View File

@ -0,0 +1,5 @@
import v1 from './v1'
export default {
v1,
}

View File

@ -0,0 +1,73 @@
import request from '@/utils/http'
import snakecaseKeys from 'snakecase-keys'
export interface CreateCommentParameters {
author?: string // The ID of the user object, if author was a user.
authorEmail?: string // Email address for the object author.
authorIp?: string // IP address for the object author.
authorName?: string // Display name for the object author.
authorUrl?: string // URL for the object author.
authorUserAgent?: string // User agent for the object author.
content: string // * The content for the object.
date?: string // The date the object was published, in the site's timezone.
dateGmt?: string // The date the object was published, as GMT.
parent: number // * The ID for the parent of the object.
post: number // * The ID of the associated post object.
status?: string // State of the object.
meta?: { [key: string]: any } // Meta fields.
}
export interface UpdateCommentParameters {
id: number // Unique identifier for the object.
author: string // The ID of the user object, if author was a user.
authorEmail: string // Email address for the object author.
authorIp: string // IP address for the object author.
authorName: string // Display name for the object author.
authorUrl: string // URL for the object author.
authorUserAgent: string // User agent for the object author.
content: string // The content for the object.
date: string // The date the object was published, in the site's timezone.
dateGmt: string // The date the object was published, as GMT.
parent: number // The ID for the parent of the object.
post: number // The ID of the associated post object.
status: string // State of the object.
meta: { [key: string]: any } // Meta fields.
}
export default {
getInitState(): Promise<any> {
return request({
url: '/sakura/v1/init-state',
method: 'GET',
})
},
getMenu({ location }: { location?: string }): Promise<any> {
return request({
url: '/sakura/v1/menu',
method: 'GET',
params: {
location,
},
})
},
createComment<T extends CreateCommentParameters>(
{ authorEmail, authorName, authorUrl, content, parent, post }: T,
auth?: { username: string; password: string }
): Promise<any> {
const form = snakecaseKeys({ authorEmail, authorName, authorUrl, content, parent, post })
const formData = new FormData()
Object.keys(form).forEach((key) => {
if (form[key] !== undefined) formData.append(key, form[key])
})
return request({
url: '/sakura/v1/comments',
method: 'POST',
data: formData,
auth,
})
},
}

View File

@ -0,0 +1,5 @@
import v2 from './v2'
export default {
v2,
}

View File

@ -0,0 +1,125 @@
import request, { AxiosPromise } from '@/utils/http'
import snakecaseKeys from 'snakecase-keys'
type WpPostObjectFilter = number | string | number[] | string[]
/**
* GET /wp/v2/posts
* https://developer.wordpress.org/rest-api/reference/posts/#arguments
*/
export interface GetPostParams {
context?: 'view' | 'embed' | 'edit'
page?: number
perPage?: number
search?: string
after?: string // ISO8601
author?: string | number
authorExclude?: string | number
before?: string // ISO8601
exclude?: WpPostObjectFilter // TODO: check this
include?: WpPostObjectFilter // TODO: check this
offset?: number
order?: 'asc' | 'desc' // default: desc
orderby?:
| 'author'
| 'date'
| 'id'
| 'include'
| 'modified'
| 'parent'
| 'relevance'
| 'slug'
| 'include_slugs'
| 'title' // default: date
slug?: WpPostObjectFilter // TODO: check this
status?: string // default: 'publish'
taxRelation?: 'AND' | 'OR'
categories?: WpPostObjectFilter // TODO: check this
categoriesExclude?: WpPostObjectFilter // TODO: check this
tags?: WpPostObjectFilter // TODO: check this
tagsExclude?: WpPostObjectFilter // TODO: check this
sticky?: boolean // TODO: check this
}
export interface GetPageParams
extends Omit<
GetPostParams,
| 'orderby'
| 'taxRelation'
| 'categories'
| 'categoriesExclude'
| 'tags'
| 'tagsExclude'
| 'sticky'
> {
menuOrder: any // TODO: check this
orderby?:
| 'author'
| 'date'
| 'id'
| 'include'
| 'modified'
| 'parent'
| 'relevance'
| 'slug'
| 'include_slugs'
| 'title'
| 'menu_order' // default: date
parent?: number[] // Limit result set to items with particular parent IDs.
parentExclude?: number[] // TODO: number[] or number? > 'Limit result set to all items except those of a particular parent ID.'
}
export interface GetCommentParams {
context?: 'view' | 'embed' | 'edit'
page?: number
perPage?: number
search?: string
after?: string // ISO8601
author?: string | number
authorExclude?: string | number
author_email?: string
before?: string // ISO8601
exclude?: WpPostObjectFilter // TODO: check this
include?: WpPostObjectFilter // TODO: check this
offset?: number
order?: 'asc' | 'desc' // default: desc
orderby?: 'date' | 'date_gmt' | 'id' | 'include' | 'post' | 'parent' | 'type' // default: date
parent?: number | number[] // Limit result set to items with particular parent IDs.
parentExclude?: number | number[] // TODO: number[] or number? > 'Limit result set to all items except those of a particular parent ID.'
post?: number | number[]
status?: string // default: 'approve'
type?: string // default: 'comment'
password?: string
}
export default {
getPosts({ ...args }: GetPostParams): AxiosPromise<any> {
return request({
url: '/wp/v2/posts',
method: 'GET',
params: snakecaseKeys({
...args,
}),
})
},
getPages({ ...args }: GetPageParams): AxiosPromise<any> {
return request({
url: '/wp/v2/pages',
method: 'GET',
params: snakecaseKeys({
...args,
}),
})
},
getComments({ ...args }: GetCommentParams): AxiosPromise<Comment[]> {
return request({
url: '/wp/v2/comments',
method: 'GET',
params: snakecaseKeys({
...args,
}),
})
},
}

15
src/api/index.ts 100644
View File

@ -0,0 +1,15 @@
import request from '@/utils/http'
import Auth from './Auth'
import Sakura from './Sakura'
import Wp from './Wp'
export default {
Auth,
Sakura,
Wp,
getWpJson(): Promise<any> {
return request({
url: '/',
method: 'GET',
})
},
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.1 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1624436304721" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="15838" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M944.835302 353.586994l-10.800419-2.486857 2.493038-10.773634c16.074946-69.444507-42.954431-133.255984-112.324764-123.724748-56.315879 7.73872-98.122688 38.501988-135.84998 80.263468H335.648018V762.684266H512.000598c16.064644 69.811252 78.722318 142.008402 120.60948 180.96573 100.970109 93.915429 204.639292 25.921416 201.645586-59.540346l-0.387348-11.047662 11.076507-0.385288c109.901779-3.828153 167.64961-171.560177-49.226173-262.896032 58.096032-5.060249 116.241513-22.618656 154.584853-46.518857 23.337722-14.542036 42.389891-33.33666 56.783581-54.743823 38.96763-57.955928 7.446149-138.882833-62.251782-154.930994z" fill="#FFB5C0" p-id="15839"></path><path d="M685.213185 91.035042C654.371624 28.395911 560.783793 15.164266 512.000598 67.338817c-48.783195-52.174551-142.371026-38.942905-173.212588 23.696225-28.177513 57.23068-22.142712 144.404604-3.139992 205.832242-37.727292-41.761481-79.536161-72.524748-135.84998-80.263469-69.370334-9.531235-128.39971 54.278181-112.324764 123.724748l2.493038 10.773634-10.800419 2.486857c-69.697932 16.048161-101.219412 96.975066-62.253843 154.933055 14.39369 21.407163 33.445859 40.201787 56.783582 54.743823 38.34334 23.898141 96.490881 41.456547 154.584853 46.518857-216.873722 91.331734-159.125891 259.063759-49.226173 262.891911l11.076507 0.385288-0.387348 11.047662c-2.991646 85.461763 100.675477 153.455775 201.645585 59.540346 41.887163-38.957328 104.544837-111.154479 120.609481-180.96573a430.852636 430.852636 0 0 1-10.363622-58.300008l137.43852-367.968322a440.163412 440.163412 0 0 1 49.277682-39.548652c19.004781-61.427638 25.039581-148.601561-3.137932-205.832242z" fill="#FF8E9E" p-id="15840"></path><path d="M327.043946 468.639678l-137.04087-78.227831a15.452716 15.452716 0 0 0-15.320853 26.838278l137.040869 78.227831z" fill="#EA5B70" p-id="15841"></path><path d="M855.077684 396.170559a15.450656 15.450656 0 0 0-21.079565-5.758712l-137.04087 78.227831 15.320854 26.840338 137.040869-78.227831a15.452716 15.452716 0 0 0 5.758712-21.081626z" fill="#FF8E9E" p-id="15842"></path><path d="M274.210078 779.212491a15.452716 15.452716 0 0 0 22.2993 21.396861l117.115107-122.062036-22.30136-21.396861z" fill="#EA5B70" p-id="15843"></path><path d="M632.667769 657.142213l-22.301361 21.396861 117.123348 122.068218a15.452716 15.452716 0 0 0 22.2993-21.396861z" fill="#FF8E9E" p-id="15844"></path><path d="M512.000598 160.364169a15.452716 15.452716 0 0 0-15.452717 15.452716V308.024145h30.905433V175.816885a15.452716 15.452716 0 0 0-15.452716-15.452716z" fill="#EA5B70" p-id="15845"></path><path d="M639.075495 336.417996H501.636976v367.968322c3.434624 0.17101 6.887791 0.259606 10.363622 0.259606 113.789682 0 206.036217-92.244475 206.036217-206.036218 0-65.845054-30.886889-124.47472-78.96132-162.19171z" fill="#FFDBE0" p-id="15846"></path><path d="M512.000598 292.571429c-113.789682 0-206.036217 92.244475-206.036218 206.036217 0 110.313851 86.69592 200.378463 195.672596 205.776612 0-137.415855 34.478101-272.423147 137.438519-367.968322C604.067881 308.951308 559.947286 292.571429 512.000598 292.571429z" fill="#FFB5C0" p-id="15847"></path><path d="M382.069006 572.924881a41.207243 32.815388 0.11 1 0 0.126002-65.630656 41.207243 32.815388 0.11 1 0-0.126002 65.630656Z" fill="#FF8E9E" p-id="15848"></path><path d="M641.875527 507.951388c-22.758761-0.049449-41.238149 14.601787-41.279356 32.724733-0.039147 18.122946 18.37637 32.856596 41.135131 32.906044s41.240209-14.601787 41.279356-32.724732-18.37637-32.854535-41.135131-32.906045z" fill="#FF8E9E" p-id="15849"></path><path d="M399.638746 476.879066a15.452716 15.452716 0 0 0-15.452716 15.452717v24.724346a15.452716 15.452716 0 0 0 30.905433 0v-24.724346a15.452716 15.452716 0 0 0-15.452717-15.452717zM624.006006 476.879066a15.452716 15.452716 0 0 0-15.452716 15.452717v24.724346a15.452716 15.452716 0 0 0 30.905432 0v-24.724346a15.452716 15.452716 0 0 0-15.452716-15.452717zM565.767809 517.843187a15.452716 15.452716 0 0 0-15.452717 15.452716c0 6.249078-5.082913 11.331992-11.331991 11.331992s-11.331992-5.082913-11.331992-11.331992a15.452716 15.452716 0 0 0-30.905433 0c0 6.249078-5.082913 11.331992-11.331992 11.331992s-11.331992-5.082913-11.331992-11.331992a15.452716 15.452716 0 0 0-30.905433 0c0 35.565972 41.547203 55.221827 69.022133 32.629956 27.470809 22.589811 69.022133 2.940137 69.022133-32.629956a15.452716 15.452716 0 0 0-15.452716-15.452716z" fill="#313D40" p-id="15850"></path></svg>

After

Width:  |  Height:  |  Size: 4.7 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.5 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.7 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.3 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.6 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.4 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 10 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1624436330392" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="16714" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M668.760596 300.099992l-3.910568-0.900378 0.902439-3.902326c5.822584-25.150841-15.555734-48.261924-40.681851-44.808757-20.395525 2.802093-35.537127 13.944531-49.201449 29.06965l-16.128515-16.128515h-90.021344v184.835091H512c5.818463 25.282704 28.511292 51.430761 43.681738 65.542181 36.569368 34.014519 74.115348 9.38701 73.031598-21.563751l-0.140105-4.001223 4.011526-0.140105c39.804137-1.386624 60.718873-62.134342-17.828314-95.213457 21.040419-1.831662 42.09938-8.192 55.986221-16.847581a68.119694 68.119694 0 0 0 20.564475-19.826865c14.113481-20.99303 2.697014-50.301682-22.546543-56.113964z" fill="#FFDBE0" p-id="16715"></path><path d="M574.733907 205.010157c-11.169223-22.684588-45.066302-27.47905-62.733907-8.581409-17.667606-18.897642-51.562624-14.103179-62.733907 8.581409-10.204974 20.727243-8.01893 52.300233-1.13732 74.545964-13.664322-15.125119-28.805924-26.265497-49.201449-29.06965-25.124056-3.453167-46.502374 19.657915-40.681851 44.808756l0.902438 3.902326-3.910567 0.900379c-25.243557 5.812282-36.657964 35.120934-22.546543 56.111903a68.09497 68.09497 0 0 0 20.564475 19.826865c13.886841 8.655581 34.945803 15.013859 55.986221 16.847582-78.545127 33.079115-57.63039 93.826833-17.828314 95.213456l4.011525 0.140105-0.140104 4.001223c-1.083751 30.952821 36.462229 55.57827 73.031597 21.563751 15.170447-14.10936 37.863276-40.257416 43.681739-65.542181V343.423227c0-24.790278 14.132024-46.263372 34.770672-56.849513a63.578656 63.578656 0 0 1 29.100555-7.019654c6.88367-22.24573 9.069714-53.81666-1.13526-74.543903z" fill="#FFB5C0" p-id="16716"></path><path d="M564.765875 299.856869a74.568628 74.568628 0 0 0-17.995203-13.281094h-56.045972v119.365022l21.27736 21.27736c19.101618 0 38.199115-7.266897 52.763815-21.831598a74.700491 74.700491 0 0 0 11.744065-15.240499c16.639485-28.566922 12.730978-65.814149-11.744065-90.289191z" fill="#FFB5C0" p-id="16717"></path><path d="M546.770672 286.575775c-28.676121-15.108636-65.42062-10.357441-89.263131 15.098334-28.222841 30.1534-26.724958 77.957924 4.330882 106.184885v0.00206h0.00206c14.206197 12.920531 32.184918 19.357103 50.161577 19.357103v-83.792869c-0.00206-24.788217 14.129964-46.263372 34.768612-56.849513z" fill="#FF8E9E" p-id="16718"></path><path d="M618.623742 15.693779h-173.729738v114.059589a247.2414 247.2414 0 0 1 67.105996-9.224241c136.286777 0 247.243461 110.515767 247.243461 247.243461 0 136.729755-110.958744 247.243461-247.243461 247.24346a246.205038 246.205038 0 0 1-82.525746-14.173231v376.03876h189.161851a144.524105 144.524105 0 0 0 23.183195-34.202012l201.221151-414.49542C866.575968 479.707944 879.774648 425.283477 879.774648 367.774648c0-166.038406-110.029521-306.375855-261.150906-352.080869z" fill="#FF8E9E" p-id="16719"></path><path d="M429.474254 600.842817C333.947622 566.935437 264.756539 475.853006 264.756539 367.774648c0-113.179815 76.165408-208.751775 180.137465-238.019219 45.189924-53.182068 105.237119-93.344708 173.729738-114.05959C584.87295 5.484684 549.082398 0 512 0 308.883316 0 144.225352 164.657964 144.225352 367.774648c0 57.508829 13.19868 111.933296 36.734197 160.409497l201.221151 414.49542C405.547268 990.815807 454.899123 1024 512 1024c42.251847 0 80.259348-18.168274 106.636105-47.118423a144.583855 144.583855 0 0 1-23.20998-34.202012z" fill="#EA5B70" p-id="16720"></path><path d="M389.624789 697.786978c-29.135581 0-52.755573 18.809046-52.755574 42.012845s23.619992 42.012845 52.755574 42.012845 52.755573-18.809046 52.755573-42.012845c0.00206-23.201738-23.617932-42.012845-52.755573-42.012845zM634.375211 697.786978c-29.135581 0-52.755573 18.809046-52.755573 42.012845s23.619992 42.012845 52.755573 42.012845 52.755573-18.809046 52.755574-42.012845c0-23.201738-23.619992-42.012845-52.755574-42.012845z" fill="#FFDBE0" p-id="16721"></path><path d="M403.936064 663.560241a15.452716 15.452716 0 0 0-15.452716 15.452717v24.724346c0 8.53608 6.918696 15.452716 15.452716 15.452716s15.452716-6.916636 15.452717-15.452716v-24.724346a15.452716 15.452716 0 0 0-15.452717-15.452717zM620.063936 663.560241a15.452716 15.452716 0 0 0-15.452717 15.452717v24.724346c0 8.53608 6.918696 15.452716 15.452717 15.452716s15.452716-6.916636 15.452716-15.452716v-24.724346a15.452716 15.452716 0 0 0-15.452716-15.452717zM556.075268 691.352467a15.452716 15.452716 0 0 0-21.802753 1.497883c-5.622728 6.453054-13.740555 10.153465-22.272515 10.153465s-16.649787-3.70041-22.272515-10.153465a15.452716 15.452716 0 0 0-23.300636 20.304869c11.494761 13.188378 28.1054 20.751968 45.573151 20.751968s34.07839-7.56359 45.573151-20.751968a15.452716 15.452716 0 0 0-1.497883-21.802752z" fill="#313D40" p-id="16722"></path></svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1624436334331" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="16853" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M961.468008 336.875396l-8.529899-1.963525 1.969706-8.509296c12.698012-54.853022-33.927984-105.255662-88.723316-97.727098-44.483219 6.113095-77.504644 30.413006-107.305722 63.399404H480.282785V660.016419h139.296966c12.689771 55.143533 62.18173 112.170237 95.267026 142.941746 79.754559 74.18334 161.639533 20.475879 159.276297-47.029827l-0.304933-8.725634 8.750358-0.304933c86.809239-3.024612 132.423598-135.51208-38.883155-207.657723 45.888386-3.997103 91.81798-17.867461 122.105304-36.744499 18.43406-11.486519 33.482946-26.331429 44.852024-43.240821 30.77769-45.779187 5.878213-109.701924-49.174664-122.379332z" fill="#FFB5C0" p-id="16854"></path><path d="M756.398101 129.489642c-24.361722-49.477537-98.285457-59.929755-136.81835-18.71633-38.532893-41.211364-112.456628-30.759147-136.818351 18.71633-22.258093 45.204346-17.490414 114.06371-2.480676 162.585239-29.801078-32.986398-62.824563-57.28631-107.305722-63.399404-54.795332-7.528563-101.421328 42.874076-88.723316 97.727098l1.969706 8.509296-8.529899 1.963525c-55.052877 12.675348-79.952354 76.598085-49.172604 122.377272 11.369078 16.909392 26.417964 31.754302 44.852024 43.240821 30.287324 18.877038 76.214857 32.747396 122.105304 36.744499-171.306753 72.143581-125.692394 204.633111-38.883155 207.657722l8.750359 0.304934-0.304934 8.725634c-2.363235 67.505706 79.521738 121.213167 159.276298 47.029827 33.085296-30.773569 82.577256-87.800274 95.267026-142.941747-3.805489-15.221956-3.490254-69.683509-5.157087-85.123863l81.400789-220.780169 63.053264-62.035445c15.009738-48.521529 19.775356-117.378833-2.480676-162.585239z" fill="#FF8E9E" p-id="16855"></path><path d="M695.825513 354.110326h-82.463936l1.063147 220.780169c2.060362 0.103018-2.085087 0 0 0 68.274221 0 128.778817-55.190922 128.778817-123.467203-0.00206-39.503324-18.535018-74.681948-47.378028-97.312966z" fill="#FFDBE0" p-id="16856"></path><path d="M619.579751 327.803622c-68.274221 0-123.62173 55.347509-123.621731 123.62173 0 66.189135 53.081111 120.226254 118.464644 123.467203 0-82.449513 19.622889-163.454712 81.400789-220.780169a123.077795 123.077795 0 0 0-76.243702-26.308764z" fill="#FFB5C0" p-id="16857"></path><path d="M456.835863 451.425352a247.793577 247.793577 0 0 0-60.992901-7.588314c-107.734278 0-199.368885 68.855243-233.346318 164.952596v0.010302C73.950519 609.974342 0 682.309537 0 773.779316c0 91.111276 73.859863 164.969078 164.971139 164.969078h351.034205V510.594833z" fill="#D4D4FF" p-id="16858"></path><path d="M643.265674 691.286535c-0.006181-115.870648-79.445505-212.728274-186.429811-239.861183-107.132652 27.200901-186.388604 124.276926-186.388603 239.871485 0 136.035412 109.765795 246.419316 245.558084 247.453618h127.629135c68.333972 0 123.728869-55.394897 123.728869-123.728869-0.00206-69.228169-56.579606-124.095614-124.097674-123.735051z" fill="#EFEDFF" p-id="16859"></path><path d="M178.046197 749.17035a61.810865 44.297787 0 1 0 123.621731 0 61.810865 44.297787 0 1 0-123.621731 0Z" fill="#FF8E9E" p-id="16860"></path><path d="M495.341972 749.17035a61.810865 44.297787 0 1 0 123.62173 0 61.810865 44.297787 0 1 0-123.62173 0Z" fill="#FF8E9E" p-id="16861"></path><path d="M269.839453 666.003831a15.452716 15.452716 0 0 0-15.452717 15.452716v24.724346c0 8.53608 6.918696 15.452716 15.452717 15.452717s15.452716-6.916636 15.452716-15.452717v-24.724346a15.452716 15.452716 0 0 0-15.452716-15.452716zM527.172507 666.003831a15.452716 15.452716 0 0 0-15.452716 15.452716v24.724346c0 8.53608 6.918696 15.452716 15.452716 15.452717s15.452716-6.916636 15.452716-15.452717v-24.724346a15.452716 15.452716 0 0 0-15.452716-15.452716zM442.580217 693.796056a15.452716 15.452716 0 0 0-21.802752 1.497884 29.545594 29.545594 0 0 1-22.272515 10.153464 29.549714 29.549714 0 0 1-22.274576-10.153464 15.452716 15.452716 0 1 0-23.298575 20.304869c11.494761 13.188378 28.1054 20.751968 45.57109 20.751968s34.07633-7.56359 45.571091-20.751968a15.450656 15.450656 0 0 0-1.493763-21.802753z" fill="#313D40" p-id="16862"></path></svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1624436312737" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="16127" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M828.952663 240.032193H576.916799a97.867203 97.867203 0 0 0-69.199324 28.661698L438.858111 344.424563v47.732411h467.016113l20.603622-47.732411v-6.867187c0-53.864048-43.663195-97.525183-97.525183-97.525183z" fill="#D6948C" p-id="16128"></path><path d="M195.049274 240.032193c-53.861988 0-97.523123 43.661135-97.523123 97.523123v6.867187l20.603622 47.73241h348.887308l20.603622-47.73241v-6.867187c0-53.861988 43.663195-97.523123 97.523123-97.523123z" fill="#CE857A" p-id="16129"></path><path d="M438.858111 344.424563V448.128773l48.762592 69.366213h386.641384l52.215759-69.366213v-103.70421z" fill="#E8B5B1" p-id="16130"></path><path d="M97.524091 344.424563V448.128773l38.62767 69.366213h312.841272l38.62767-69.366213v-103.70421z" fill="#D6948C" p-id="16131"></path><path d="M487.620703 448.128773l-163.432049 61.124764 163.432049 43.953706h292.571428V834.44668c0 40.395461 32.747396 73.142857 73.142857 73.142857s73.142857-32.747396 73.142858-73.142857v-146.285714-240.032193z" fill="#D6948C" p-id="16132"></path><path d="M97.524091 448.128773V834.44668c0 40.395461 32.747396 73.142857 73.142857 73.142857s73.142857-32.747396 73.142857-73.142857V553.207243h243.808837v-105.07847z" fill="#CE857A" p-id="16133"></path><path d="M950.858111 615.018109H487.620703L365.715254 688.160966l121.905449 73.142857H950.858111c40.395461 0 73.142857-32.747396 73.142857-73.142857s-32.747396-73.142857-73.142857-73.142857z" fill="#E8B5B1" p-id="16134"></path><path d="M487.620703 615.018109H75.210369C35.826546 615.018109 1.937709 645.284829 0.085443 684.623324-1.894565 726.627928 31.576019 761.303823 73.143826 761.303823h414.476877c-40.395461-40.395461-40.395461-105.890254 0-146.285714z" fill="#D6948C" p-id="16135"></path><path d="M287.602803 475.387364a60.481932 48.165087 0 1 0 120.963864 0 60.481932 48.165087 0 1 0-120.963864 0Z" fill="#FFDBE0" p-id="16136"></path><path d="M615.43527 475.387364a60.481932 48.165087 0 1 0 120.963863 0 60.481932 48.165087 0 1 0-120.963863 0Z" fill="#FFDBE0" p-id="16137"></path><path d="M383.333411 388.845972a15.452716 15.452716 0 0 0-15.452716 15.452716v24.724346c0 8.53608 6.918696 15.452716 15.452716 15.452717s15.452716-6.916636 15.452716-15.452717v-24.724346a15.452716 15.452716 0 0 0-15.452716-15.452716zM640.668526 388.845972a15.452716 15.452716 0 0 0-15.452717 15.452716v24.724346c0 8.53608 6.918696 15.452716 15.452717 15.452717s15.452716-6.916636 15.452716-15.452717v-24.724346a15.452716 15.452716 0 0 0-15.452716-15.452716zM556.076236 416.636137a15.452716 15.452716 0 0 0-21.802753 1.497883c-5.622728 6.455115-13.740555 10.155525-22.272515 10.155525s-16.649787-3.70041-22.272515-10.155525a15.452716 15.452716 0 0 0-23.300636 20.304869c11.494761 13.188378 28.1054 20.754028 45.573151 20.754029s34.07839-7.56565 45.573151-20.754029a15.452716 15.452716 0 0 0-1.497883-21.802752z" fill="#313D40" p-id="16138"></path><path d="M380.48187 178.221328c0-20.42231-9.910342-38.524652-25.177626-49.78041h-41.207243L253.837588 239.129755l59.410543 113.09534h41.207244c15.745288-11.204249 26.026495-29.586801 26.026495-50.382037 34.138141 0 61.810865-27.674785 61.810865-61.810865s-27.674785-61.810865-61.810865-61.810865z" fill="#FFDBE0" p-id="16139"></path><path d="M295.044832 239.129755c0.500668-45.134294 23.523155-86.370382 60.257352-110.688837a61.522414 61.522414 0 0 0-36.63324-12.030455c-34.138141 0-61.810865 27.674785-61.810865 61.810865-34.138141 0-61.810865 27.674785-61.810865 61.810865s27.672724 61.810865 61.810865 61.810865c0 34.13608 27.672724 61.810865 61.810865 61.810866a61.49563 61.49563 0 0 0 35.78437-11.428829c-37.142149-24.994254-59.915332-67.293489-59.408482-113.09534z" fill="#FFB5C0" p-id="16140"></path><path d="M318.671005 240.032193m-23.632355 0a23.632354 23.632354 0 1 0 47.264709 0 23.632354 23.632354 0 1 0-47.264709 0Z" fill="#FF8E9E" p-id="16141"></path></svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.6 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.0 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.4 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.1 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.6 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.8 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 10 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.9 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1624436346366" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="17447" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M61.880176 663.106962c-21.740942 81.246262-0.91274 171.628169 63.236636 235.775485 63.813537 63.815598 154.273738 85.035268 235.779605 63.255179L297.703049 726.296209zM121.488514 68.651268l176.214535 657.644941 0.436797 0.117441L782.774115 241.77938l-0.138044-0.140104C609.534743 68.535887 356.540812 2.608419 121.488514 68.651268z" fill="#FF8E9E" p-id="17448"></path><path d="M782.774115 241.77938L298.139846 726.41159l657.210205 176.099154c65.987219-234.864805 0.216338-487.640338-172.575936-660.731364z" fill="#FFB5C0" p-id="17449"></path><path d="M360.85727 1024c-27.310101 0-52.304354-18.225964-59.707235-45.861602L59.926952 77.877569C51.085938 44.880869 70.66556 10.963187 103.66226 2.122173 136.667202-6.71472 170.578703 12.864901 179.417657 45.859541l241.223082 900.26289c8.841014 32.9967-10.740668 66.914382-43.737368 75.755396a61.959211 61.959211 0 0 1-16.046101 2.122173z" fill="#D6948C" p-id="17450"></path><path d="M81.379443 942.619815c-24.155686-24.155686-24.155686-63.31699 0-87.472676L740.204973 196.32161c24.153626-24.155686 63.31905-24.155686 87.472676 0 24.155686 24.155686 24.155686 63.31905 0 87.474736L168.852119 942.619815c-24.149505 24.153626-63.31699 24.157746-87.472676 0z" fill="#E8B5B1" p-id="17451"></path><path d="M946.123749 964.072306L45.86086 722.849223C12.86416 714.008209-6.717522 680.092588 2.123492 647.095887c8.838954-32.9967 42.750455-52.584563 75.755396-43.737368l900.262889 241.223083c32.9967 8.841014 52.578382 42.758696 43.737369 75.755396-8.836893 32.978157-42.742213 52.584563-75.755397 43.735308z" fill="#F2D4D3" p-id="17452"></path><path d="M297.703049 726.296209m-30.905433 0a30.905433 30.905433 0 1 0 61.810866 0 30.905433 30.905433 0 1 0-61.810866 0Z" fill="#FF8E9E" p-id="17453"></path><path d="M741.148618 560.027042c0-22.758761 18.448483-41.207243 41.207244-41.207243s41.207243 18.448483 41.207243 41.207243c22.758761 0 41.207243 18.448483 41.207244 41.207244s-18.448483 41.207243-41.207244 41.207243c0 22.758761-18.448483 41.207243-41.207243 41.207244s-41.207243-18.448483-41.207244-41.207244c-22.758761 0-41.207243-18.448483-41.207243-41.207243s18.448483-41.207243 41.207243-41.207244z" fill="#FFDBE0" p-id="17454"></path><path d="M782.355862 601.234286m-23.632354 0a23.632354 23.632354 0 1 0 47.264708 0 23.632354 23.632354 0 1 0-47.264708 0Z" fill="#FFB5C0" p-id="17455"></path><path d="M368.299299 196.591517c0-35.275461 28.595767-63.871227 63.871227-63.871227s63.871227 28.595767 63.871227 63.871227c35.275461 0 63.871227 28.595767 63.871228 63.871227s-28.595767 63.871227-63.871228 63.871228c0 35.275461-28.595767 63.871227-63.871227 63.871227s-63.871227-28.595767-63.871227-63.871227c-35.275461 0-63.871227-28.595767-63.871228-63.871228s28.595767-63.871227 63.871228-63.871227z" fill="#FFDBE0" p-id="17456"></path><path d="M432.170526 260.462744m-36.056338 0a36.056338 36.056338 0 1 0 72.112676 0 36.056338 36.056338 0 1 0-72.112676 0Z" fill="#FF8E9E" p-id="17457"></path></svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

Some files were not shown because too many files have changed in this diff Show More