diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000..1d98ca36 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,4 @@ +config/* +scripts/* +public/* +*.test.tsx" \ No newline at end of file diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index 0a40e475..00000000 --- a/.eslintrc +++ /dev/null @@ -1,66 +0,0 @@ -{ - "env": { - "browser": true, - "es6": true - }, - "plugins": ["@typescript-eslint", "import", "unicorn", "react", "react-hooks"], - "globals": { - "JSX": true - }, - "extends": [ - "react-app", - "plugin:@typescript-eslint/recommended", - "eslint:recommended", - "plugin:import/errors", - "plugin:import/warnings", - "plugin:import/typescript", - "plugin:react/recommended", - "plugin:unicorn/recommended", - "prettier", - "prettier/prettier" - ], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaVersion": 2020, - "sourceType": "module" - }, - "settings": { - "import/resolver": { - "alias": { - "map": [ - ["@root", "./src"], - ["@comp", "./src/components"], - ["@config", "./src/config"], - ["@elem", "./src/elements"], - ["@store", "./src/store"], - ["@styles", "./src/styles"], - ["@db", "./src/db"] - ], - "extensions": [".ts", ".tsx", ".js", ".jsx", ".json"] - } - } - }, - "rules": { - "@typescript-eslint/no-unused-vars": "error", - "@typescript-eslint/no-explicit-any": "off", - "no-var": "error", - "prefer-const": "error", - "curly": "error", - "no-unused-vars": "off", - "no-empty": "off", - "react/prop-types": "off", - "unicorn/no-reduce": "off", - "unicorn/no-array-for-each": "off", - "unicorn/no-array-reduce": "off", - "unicorn/no-array-callback-reference": "off", - "unicorn/prevent-abbreviations": "off", - "react/jsx-uses-react": "error", - "react-hooks/exhaustive-deps": "error" - }, - "ignorePatterns": [ - "**/src/react-app-env.d.ts", - "**/src/styles/*", - "**/src/service-worker.js", - "**/plugins/show-hint.js" - ] -} diff --git a/.github/workflows/develop.yaml b/.github/workflows/develop.yaml index acb85b15..ec56d01b 100644 --- a/.github/workflows/develop.yaml +++ b/.github/workflows/develop.yaml @@ -7,20 +7,27 @@ jobs: deploy-dev: name: deploy-develop runs-on: ubuntu-latest + env: + REACT_APP_DATABASE: DEV steps: - name: Checkout Repo uses: actions/checkout@master + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: yarn - name: Build run: | yarn install + echo "STORAGE_BUCKET_URL=gs://csound-ide-dev.appspot.com" >> functions/.env.dev PUBLIC_URL="https://csound-ide-dev.web.app" yarn build:dev - cp build/index.html functions + cp dist/index.html functions cd functions yarn install + yarn build - name: Deploy to Firebase uses: w9jds/firebase-action@master with: args: deploy -P develop env: GCP_SA_KEY: ${{ secrets.GCP_SA_KEY_DEV }} - REACT_APP_DATABASE: DEV diff --git a/.github/workflows/production.yaml b/.github/workflows/production.yaml index fda93520..e601dea0 100644 --- a/.github/workflows/production.yaml +++ b/.github/workflows/production.yaml @@ -7,16 +7,20 @@ jobs: deploy-prod: name: deploy-production runs-on: ubuntu-latest + env: + REACT_APP_DATABASE: PROD steps: - name: Checkout Repo uses: actions/checkout@master - name: Build run: | yarn install + echo "STORAGE_BUCKET_URL=gs://csound-ide.appspot.com" >> functions/.env PUBLIC_URL="https://ide.csound.com" PRODUCTION=1 yarn build - cp build/index.html functions + cp dist/index.html functions cd functions yarn install + yarn build - name: Deploy to Firebase uses: w9jds/firebase-action@master with: diff --git a/.gitignore b/.gitignore index 4af8c3c0..55121cdf 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ # Firebase .firebase +firebase-debug.log # dependencies /node_modules @@ -11,9 +12,13 @@ # production /build +dist # misc .DS_Store +.env +.env.prod +.env.dev .env.local .env.development.local .env.test.local diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 00000000..bbbc8677 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +npm run precommit-hook diff --git a/config/paths.js b/config/paths.js index b3fd764a..4a337982 100644 --- a/config/paths.js +++ b/config/paths.js @@ -52,7 +52,7 @@ const resolveModule = (resolveFn, filePath) => { module.exports = { dotenv: resolveApp('.env'), appPath: resolveApp('.'), - appBuild: resolveApp('build'), + appBuild: resolveApp('dist'), appPublic: resolveApp('public'), appHtml: resolveApp('public/index.html'), appIndexJs: resolveModule(resolveApp, 'src/index'), diff --git a/config/webpack.config.js b/config/webpack.config.js index 0c88719b..ee621d95 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -14,15 +14,16 @@ const TerserPlugin = require("terser-webpack-plugin"); const { WebpackManifestPlugin } = require("webpack-manifest-plugin"); const InterpolateHtmlPlugin = require("react-dev-utils/InterpolateHtmlPlugin"); const paths = require("./paths"); -const WorkboxWebpackPlugin = require("workbox-webpack-plugin"); +// const WorkboxWebpackPlugin = require("workbox-webpack-plugin"); const getClientEnvironment = require("./env"); const ModuleNotFoundPlugin = require("react-dev-utils/ModuleNotFoundPlugin"); const ForkTsCheckerWebpackPlugin = require("react-dev-utils/ForkTsCheckerWarningWebpackPlugin"); const RobotstxtPlugin = require("robotstxt-webpack-plugin"); const SitemapPlugin = require("sitemap-webpack-plugin").default; const ESLintPlugin = require("eslint-webpack-plugin"); +const ReactRefreshWebpackPlugin = require("@pmmmwh/react-refresh-webpack-plugin"); -const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== "false"; +// const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== "false"; const shouldInlineRuntimeChunk = false; const imageInlineSizeLimit = parseInt( @@ -41,7 +42,7 @@ module.exports = function (webpackEnv, env_ = {}) { const isEnvProductionProfile = isEnvProduction && process.argv.includes("--profile"); - console.log({ isEnvProduction, mode }); + // console.log({ isEnvProduction, mode }); const env = getClientEnvironment(paths.publicUrlOrPath.slice(0, -1)); return { @@ -109,7 +110,6 @@ module.exports = function (webpackEnv, env_ = {}) { "@comp": path.resolve(__dirname, "../src/components"), "@elem": path.resolve(__dirname, "../src/elements"), "@config": path.resolve(__dirname, "../src/config"), - "@store": path.resolve(__dirname, "../src/store"), history: process.cwd() + "/node_modules/history", "react-native": "react-native-web", react: process.cwd() + "/node_modules/react", @@ -119,6 +119,7 @@ module.exports = function (webpackEnv, env_ = {}) { process.cwd() + "/node_modules/react-router-dom", "@emotion/react": process.cwd() + "/node_modules/@emotion/react", + "@codemirror": process.cwd() + "/node_modules/@codemirror", // Allows for better profiling with ReactDevTools ...(isEnvProductionProfile && { "react-dom$": "react-dom/profiling", @@ -134,10 +135,10 @@ module.exports = function (webpackEnv, env_ = {}) { }, module: { rules: [ - { - test: /\.(grammar|terms|terms\.js)$/, - use: require.resolve("./lezer-loader.js") - }, + // { + // test: /\.(grammar|terms|terms\.js)$/, + // use: require.resolve("./lezer-loader.js") + // }, { enforce: "pre", exclude: /@babel(?:\/|\\{1,2})runtime/, @@ -168,7 +169,15 @@ module.exports = function (webpackEnv, env_ = {}) { exclude: path.resolve(__dirname, "../node_modules/"), use: [ - { loader: "babel-loader", options: { babelrc: true } } + { + loader: "babel-loader", + options: isEnvDevelopment + ? { + babelrc: true, + plugins: ["react-refresh/babel"] + } + : { babelrc: true } + } ] }, { @@ -215,6 +224,7 @@ module.exports = function (webpackEnv, env_ = {}) { ] }, plugins: [ + new ReactRefreshWebpackPlugin(), // Generates an `index.html` file with the diff --git a/jest.config.js b/jest.config.js index 81ce1486..b069d5fe 100644 --- a/jest.config.js +++ b/jest.config.js @@ -25,7 +25,6 @@ module.exports = { "^@comp(.*)$": "/src/components$1", "^@elem(.*)$": "/src/elements$1", "^@config(.*)$": "/src/config$1", - "^@store(.*)$": "/src/store$1", "^.*\\.svg$": "/__mocks__/svgMock.js", "\\.(jpg|jpeg|png|gif|orc|sco|csd|udo)$": "/__mocks__/fileMock.js", diff --git a/package.json b/package.json index ac378c06..25c949b6 100644 --- a/package.json +++ b/package.json @@ -1,201 +1,161 @@ { "name": "web-ide", "version": "0.1.0", + "type": "module", "private": true, "main": "public/electron.js", "scripts": { - "start": "cross-env REACT_APP_DATABASE=DEV node scripts/start.js", - "build": "cross-env NODE_ENV=production REACT_APP_DATABASE=PROD npx webpack --mode production -c config/webpack.config.js", - "build:dev": "cross-env NODE_ENV=production REACT_APP_DATABASE=DEV npx webpack --mode production -c config/webpack.config.js", + "start": "cross-env REACT_APP_DATABASE=DEV vite", + "start:prod": "cross-env REACT_APP_DATABASE=PROD vite", + "build": "cross-env NODE_ENV=production REACT_APP_DATABASE=PROD tsc && vite build", + "build:dev": "cross-env NODE_ENV=production REACT_APP_DATABASE=DEV tsc && vite build", "test": "cross-env REACT_APP_DATABASE=DEV jest", "lint": "prettier --write src/**/*.{js,jsx,ts,tsx}", - "lint:check": "eslint --ext js,jsx,ts,tsx src", - "electron:dev": "concurrently \"BROWSER=none yarn start\" \"wait-on http://localhost:3000 && ELECTRON_IS_DEV=1 electron .\"", + "lint:check": "eslint ./src", + "electron:dev": "concurrently \"BROWSER=none yarn start\" \"npx wait-on http-get://127.0.0.1:3000 && ELECTRON_IS_DEV=1 npx electron .\"", "deploy": "./node_modules/.bin/firebase deploy -P default --token=$FIREBASE_TOKEN", "deploy:dev": "./node_modules/.bin/firebase deploy -P develop --token=$FIREBASE_TOKEN", - "postinstall": "patch-package", - "prepare": "husky install" + "precommit-hook": "npm run lint:check", + "prepare": "husky" }, "dependencies": { - "@codemirror/text": "^0.19.6", - "@csound/browser": "^6.18.0-beta1", - "@hlolli/react-modal-resizable-draggable": "^0.1.5", - "@hlolli/react-tabtab": "^3.0.2", - "@lezer/generator": "^1.1.1", - "@mui/icons-material": "^5.8.4", - "@mui/material": "^5.9.2", - "@uiw/codemirror-themes": "^4.13.0", - "@uiw/react-codemirror": "^4.13.0", - "connected-react-router": "^6.9.2", - "history": "^5.3.0", - "mime": "^3.0.0", - "ramda": "^0.28.0", - "react": "^18.0.0", + "@csound/browser": "^6.18.7", + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@emotion/core": "^11.0.0", + "@emotion/css": "^11.13.5", + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.1", + "@hlolli/codemirror-lang-csound": "^1.0.0-alpha10", + "@mui/icons-material": "^6.2.0", + "@mui/material": "^6.2.0", + "@reduxjs/toolkit": "^2.5.0", + "@vitejs/plugin-react": "^4.3.4", + "connected-react-router": "^6.9.3", + "csound7": "npm:@csound/browser@^7.0.0-beta7", + "date-fns": "^4.1.0", + "firebase-functions": "^6.4.0", + "history": "5.3.0", + "mime": "^4.0.4", + "ramda": "^0.30.1", + "react": "^19.0.0", "react-autosuggest": "^10.1.0", - "react-beautiful-dnd": "^13.1.0", - "react-beforeunload": "^2.5.3", + "react-beautiful-dnd": "^13.1.1", + "react-beforeunload": "^2.6.0", "react-color": "^2.19.3", - "react-debounce-input": "^3.2.5", + "react-debounce-input": "^3.3.0", "react-dev-utils": "^12.0.1", - "react-dom": "^18.0.0", + "react-dom": "^19.0.0", "react-firebaseui": "^6.0.0", "react-hotkeys": "^2.0.0", "react-iframe-comm": "^1.2.2", - "react-loader-spinner": "^5.1.4", - "react-moment": "^1.1.2", - "react-onclickoutside": "^6.12.1", + "react-loader-spinner": "^6.1.6", "react-perfect-scrollbar": "^1.5.8", "react-piano": "^3.1.3", "react-poppop": "^1.5.0", - "react-range-slider-input": "^2.1.4", - "react-redux": "^7.2.8", - "react-refresh": "^0.12.0", - "react-router": "^6.3.0", - "react-router-dom": "^6.3.0", - "react-select": "^5.3.0", - "react-share": "^4.4.0", - "react-sortable-hoc": "^2.0.0", + "react-redux": "^9.2.0", + "react-refresh": "^0.16.0", + "react-router": "^7.0.2", + "react-select": "^5.9.0", + "react-share": "^5.1.1", "react-split-pane": "^0.1.92", - "react-tooltip": "^4.2.21", "react-use-storage": "^0.5.1", - "recharts": "^2.1.15", - "redux": "^4.2.0", - "redux-first-history": "^5.0.9", - "redux-thunk": "^2.4.1", - "url": "^0.11.0" + "redux": "^5.0.1", + "redux-first-history": "^5.2.0", + "url": "^0.11.4", + "vite": "^6.0.3", + "vite-raw-plugin": "^1.0.2", + "vite-tsconfig-paths": "^5.1.4", + "wait-on": "^8.0.1" }, "devDependencies": { - "@babel/core": "7.17.9", - "@babel/helper-builder-react-jsx": "^7.16.7", + "@babel/core": "7.26.0", + "@babel/helper-builder-react-jsx": "^7.25.9", "@babel/helper-builder-react-jsx-experimental": "^7.12.11", "@babel/helper-regex": "^7.10.5", - "@babel/plugin-transform-runtime": "^7.17.0", - "@babel/preset-env": "^7.16.11", - "@babel/preset-react": "^7.16.7", - "@babel/preset-typescript": "^7.16.7", - "@babel/runtime": "^7.17.9", - "@emotion/babel-preset-css-prop": "^11.2.0", - "@emotion/core": "^11.0.0", - "@emotion/css": "^11.9.0", - "@emotion/react": "^11.9.0", - "@emotion/styled": "^11.8.1", - "@fortawesome/fontawesome-svg-core": "^6.1.1", - "@fortawesome/free-solid-svg-icons": "^6.1.1", - "@fortawesome/react-fontawesome": "^0.1.18", - "@material-ui/core": "^4.12.4", - "@material-ui/icons": "^4.11.3", - "@material-ui/styles": "^4.11.5", - "@pmmmwh/react-refresh-webpack-plugin": "^0.5.5", - "@sentry/browser": "^6.19.6", - "@svgr/webpack": "^6.2.1", - "@testing-library/react": "^13.1.1", - "@types/jest": "^27.4.1", - "@types/lodash": "^4.14.182", - "@types/ramda": "types/npm-ramda", - "@types/react": "^18.0.6", - "@types/react-dom": "^18.0.2", - "@types/react-onclickoutside": "^6.7.4", - "@types/react-redux": "^7.1.24", - "@types/styled-components": "^5.1.25", - "@typescript-eslint/eslint-plugin": "^5.20.0", - "@typescript-eslint/parser": "^5.20.0", + "@babel/plugin-transform-runtime": "^7.25.9", + "@babel/preset-env": "^7.26.0", + "@babel/preset-react": "^7.26.3", + "@babel/preset-typescript": "^7.26.0", + "@babel/runtime": "^7.26.0", + "@emotion/babel-preset-css-prop": "^11.12.0", + "@eslint/js": "^9.17.0", + "@sentry/browser": "^8.45.0", + "@testing-library/react": "^16.1.0", + "@types/eslint__js": "^8.42.3", + "@types/jest": "^29.5.14", + "@types/lodash": "^4.17.13", + "@types/node": "^22.10.2", + "@types/ramda": "^0.30.2", + "@types/react": "^19.0.1", + "@types/react-color": "^3.0.13", + "@types/react-dom": "^19.0.2", + "@types/react-onclickoutside": "^6.7.10", + "@types/react-redux": "^7.1.34", + "@types/throttle-debounce": "^5.0.2", + "@typescript-eslint/eslint-plugin": "^8.18.0", + "@typescript-eslint/parser": "^8.18.0", "array-move": "^4.0.0", - "autosuggest-highlight": "^3.2.1", + "autosuggest-highlight": "^3.3.4", "babel-eslint": "10.1.0", - "babel-loader": "8.2.5", + "babel-loader": "9.2.1", "babel-plugin-named-asset-import": "^0.3.8", - "camelcase": "^6.3.0", - "case-sensitive-paths-webpack-plugin": "2.4.0", + "camelcase": "^8.0.0", "codemirror": "^6.0.1", - "concurrently": "^7.1.0", - "copy-webpack-plugin": "^10.2.4", + "concurrently": "^9.1.0", "cross-env": "^7.0.3", - "css-loader": "6.7.1", + "css-loader": "7.1.2", "d3-scale": "^4.0.2", - "dom-to-image-more": "^2.9.5", - "dotenv": "16.0.0", - "dotenv-expand": "8.0.3", - "electron": "^18.1.0", - "electron-builder": "^23.0.3", - "electron-is-dev": "^2.0.0", - "eslint": "^8.14.0", - "eslint-config-prettier": "^8.5.0", + "dom-to-image-more": "^3.5.0", + "dotenv": "16.4.7", + "dotenv-expand": "12.0.1", + "electron": "^33.2.1", + "electron-builder": "^25.1.8", + "electron-is-dev": "^3.0.1", + "eslint": "^9.17.0", + "eslint-config-prettier": "^9.1.0", "eslint-config-react-app": "^7.0.1", "eslint-import-resolver-alias": "^1.1.2", "eslint-plugin-flowtype": "8.0.3", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-jsx-a11y": "6.5.1", - "eslint-plugin-prettier": "^4.0.0", - "eslint-plugin-react": "7.29.4", - "eslint-plugin-react-hooks": "^4.4.0", - "eslint-plugin-unicorn": "^42.0.0", - "eslint-webpack-plugin": "^3.1.1", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jsx-a11y": "6.10.2", + "eslint-plugin-prettier": "^5.2.1", + "eslint-plugin-react": "7.37.2", + "eslint-plugin-react-hooks": "^5.1.0", + "eslint-plugin-unicorn": "^56.0.1", "file-loader": "6.2.0", "file-saver": "^2.0.5", - "firebase": "^9.6.11", - "fork-ts-checker-webpack-plugin": "^7.2.7", - "fuse.js": "^6.5.3", + "firebase": "^11.1.0", + "fuse.js": "^7.0.0", + "globals": "^15.13.0", "hex-rgb": "^5.0.0", - "html-webpack-plugin": "5.5.0", - "husky": "^7.0.4", - "jest": "27.5.1", + "husky": "^9.1.7", + "jest": "29.7.0", "jest-environment-jsdom-fourteen": "1.0.1", "jest-fetch-mock": "^3.0.3", - "jest-watch-typeahead": "1.0.0", - "jszip": "^3.9.1", + "jest-watch-typeahead": "2.2.2", + "jszip": "^3.10.1", "lodash": "^4.17.21", - "material-ui-chip-input": "^2.0.0-beta.1", - "moment": "^2.29.3", - "patch-package": "^6.4.7", - "pnp-webpack-plugin": "1.7.0", - "postcss-loader": "6.2.1", - "prettier": "^2.6.2", - "raw-loader": "^4.0.2", + "moment": "^2.30.1", + "prettier": "^3.4.2", "react-app-polyfill": "^3.0.0", "react-redux-test-renderer": "^4.0.1", - "reselect": "^4.1.5", - "resize-observer-polyfill": "^1.5.1", - "resolve": "1.22.0", - "resolve-url-loader": "5.0.0", - "robotstxt-webpack-plugin": "^7.0.0", - "sitemap-webpack-plugin": "^1.1.1", - "source-map-loader": "^3.0.1", + "reselect": "^5.1.1", + "resolve": "1.22.9", "stream-browserify": "^3.0.0", - "style-loader": "3.3.1", - "styled-components": "^5.3.5", - "terser-webpack-plugin": "5.3.1", - "throttle-debounce": "^4.0.1", - "ts-jest": "^27.1.4", - "ts-loader": "^9.2.8", + "throttle-debounce": "^5.0.2", + "ts-jest": "^29.2.5", "ts-pnp": "1.2.0", - "typescript": "4.6.3", - "url-loader": "4.1.1", - "uuid": "^8.3.2", - "webpack": "5.72.0", - "webpack-cli": "^4.9.2", - "webpack-dev-server": "4.8.1", - "webpack-manifest-plugin": "5.0.0", - "workbox-webpack-plugin": "6.5.3", - "workerize-loader": "^1.3.0" + "typescript": "^5.7.2", + "typescript-eslint": "^8.18.0", + "uuid": "^11.0.3", + "vite-plugin-checker": "^0.8.0", + "vite-plugin-svgr": "^4.3.0" }, "resolutions": { - "react-iframe-comm/react": "^17.0.1", - "react-iframe-comm/react-dom": "^17.0.1", - "@babel/plugin-syntax-class-static-block@^7.0": "7.14.5" - }, - "engines": { - "node": ">=15" - }, - "browserslist": { - "production": [ - ">0.2%", - "not dead", - "not op_mini all" - ], - "development": [ - "last 1 chrome version", - "last 1 firefox version", - "last 1 safari version" - ] + "react-iframe-comm/react": "*", + "react-iframe-comm/react-dom": "*", + "@babel/plugin-syntax-class-static-block@^7.0": "7.14.5", + "@hlolli/react-codemirror/codemirror": "^6.0.1" } } diff --git a/patches/perfect-scrollbar+1.5.0.patch b/patches/perfect-scrollbar+1.5.0.patch deleted file mode 100644 index d9c05d9f..00000000 --- a/patches/perfect-scrollbar+1.5.0.patch +++ /dev/null @@ -1,11 +0,0 @@ -diff --git a/node_modules/perfect-scrollbar/dist/perfect-scrollbar.esm.js b/node_modules/perfect-scrollbar/dist/perfect-scrollbar.esm.js -index 3a4c392..c8ebea5 100644 ---- a/node_modules/perfect-scrollbar/dist/perfect-scrollbar.esm.js -+++ b/node_modules/perfect-scrollbar/dist/perfect-scrollbar.esm.js -@@ -885,6 +885,5 @@ function wheel(i) { - shouldPrevent = shouldPrevent || shouldPreventDefault(deltaX, deltaY); - if (shouldPrevent && !e.ctrlKey) { - e.stopPropagation(); -- e.preventDefault(); - } - } diff --git a/public/csound-no-audio.js b/public/csound-no-audio.js new file mode 100644 index 00000000..946d1447 --- /dev/null +++ b/public/csound-no-audio.js @@ -0,0 +1,691 @@ +/* + + Copyright The Closure Library Authors. + SPDX-License-Identifier: Apache-2.0 +*/ +var $jscomp=$jscomp||{};$jscomp.scope={};var COMPILED=!0,goog=goog||{};goog.global=this||self;goog.exportPath_=function(a,b,c,d){a=a.split(".");d=d||goog.global;a[0]in d||"undefined"==typeof d.execScript||d.execScript("var "+a[0]);for(var e;a.length&&(e=a.shift());)if(a.length||void 0===b)d=d[e]&&d[e]!==Object.prototype[e]?d[e]:d[e]={};else if(!c&&goog.isObject(b)&&goog.isObject(d[e]))for(var f in b)b.hasOwnProperty(f)&&(d[e][f]=b[f]);else d[e]=b}; +goog.define=function(a,b){if(!COMPILED){var c=goog.global.CLOSURE_UNCOMPILED_DEFINES,d=goog.global.CLOSURE_DEFINES;c&&void 0===c.nodeType&&Object.prototype.hasOwnProperty.call(c,a)?b=c[a]:d&&void 0===d.nodeType&&Object.prototype.hasOwnProperty.call(d,a)&&(b=d[a])}return b};goog.FEATURESET_YEAR=2020;goog.DEBUG=!0;goog.LOCALE="en";goog.getLocale=function(){return goog.LOCALE};goog.TRUSTED_SITE=!0;goog.DISALLOW_TEST_ONLY_CODE=COMPILED&&!goog.DEBUG;goog.ENABLE_CHROME_APP_SAFE_SCRIPT_LOADING=!1; +goog.provide=function(a){if(goog.isInModuleLoader_())throw Error("goog.provide cannot be used within a module.");if(!COMPILED&&goog.isProvided_(a))throw Error('Namespace "'+a+'" already declared.');goog.constructNamespace_(a)};goog.constructNamespace_=function(a,b,c){if(!COMPILED){delete goog.implicitNamespaces_[a];for(var d=a;(d=d.substring(0,d.lastIndexOf(".")))&&!goog.getObjectByName(d);)goog.implicitNamespaces_[d]=!0}goog.exportPath_(a,b,c)};goog.NONCE_PATTERN_=/^[\w+/_-]+[=]{0,2}$/; +goog.getScriptNonce_=function(a){a=(a||goog.global).document;return(a=a.querySelector&&a.querySelector("script[nonce]"))&&(a=a.nonce||a.getAttribute("nonce"))&&goog.NONCE_PATTERN_.test(a)?a:""};goog.VALID_MODULE_RE_=/^[a-zA-Z_$][a-zA-Z0-9._$]*$/; +goog.module=function(a){if("string"!==typeof a||!a||-1==a.search(goog.VALID_MODULE_RE_))throw Error("Invalid module identifier");if(!goog.isInGoogModuleLoader_())throw Error("Module "+a+" has been loaded incorrectly. Note, modules cannot be loaded as normal scripts. They require some kind of pre-processing step. You're likely trying to load a module via a script tag or as a part of a concatenated bundle without rewriting the module. For more info see: https://github.com/google/closure-library/wiki/goog.module:-an-ES6-module-like-alternative-to-goog.provide.");if(goog.moduleLoaderState_.moduleName)throw Error("goog.module may only be called once per module."); +goog.moduleLoaderState_.moduleName=a;if(!COMPILED){if(goog.isProvided_(a))throw Error('Namespace "'+a+'" already declared.');delete goog.implicitNamespaces_[a]}};goog.module.get=function(a){return goog.module.getInternal_(a)};goog.module.getInternal_=function(a){if(!COMPILED){if(a in goog.loadedModules_)return goog.loadedModules_[a].exports;if(!goog.implicitNamespaces_[a])return a=goog.getObjectByName(a),null!=a?a:null}return null};goog.ModuleType={ES6:"es6",GOOG:"goog"};goog.moduleLoaderState_=null; +goog.isInModuleLoader_=function(){return goog.isInGoogModuleLoader_()||goog.isInEs6ModuleLoader_()};goog.isInGoogModuleLoader_=function(){return!!goog.moduleLoaderState_&&goog.moduleLoaderState_.type==goog.ModuleType.GOOG};goog.isInEs6ModuleLoader_=function(){if(goog.moduleLoaderState_&&goog.moduleLoaderState_.type==goog.ModuleType.ES6)return!0;var a=goog.global.$jscomp;return a?"function"!=typeof a.getCurrentModulePath?!1:!!a.getCurrentModulePath():!1}; +goog.module.declareLegacyNamespace=function(){if(!COMPILED&&!goog.isInGoogModuleLoader_())throw Error("goog.module.declareLegacyNamespace must be called from within a goog.module");if(!COMPILED&&!goog.moduleLoaderState_.moduleName)throw Error("goog.module must be called prior to goog.module.declareLegacyNamespace.");goog.moduleLoaderState_.declareLegacyNamespace=!0}; +goog.declareModuleId=function(a){if(!COMPILED){if(!goog.isInEs6ModuleLoader_())throw Error("goog.declareModuleId may only be called from within an ES6 module");if(goog.moduleLoaderState_&&goog.moduleLoaderState_.moduleName)throw Error("goog.declareModuleId may only be called once per module.");if(a in goog.loadedModules_)throw Error('Module with namespace "'+a+'" already exists.');}if(goog.moduleLoaderState_)goog.moduleLoaderState_.moduleName=a;else{var b=goog.global.$jscomp;if(!b||"function"!=typeof b.getCurrentModulePath)throw Error('Module with namespace "'+ +a+'" has been loaded incorrectly.');b=b.require(b.getCurrentModulePath());goog.loadedModules_[a]={exports:b,type:goog.ModuleType.ES6,moduleId:a}}};goog.setTestOnly=function(a){if(goog.DISALLOW_TEST_ONLY_CODE)throw a=a||"",Error("Importing test-only code into non-debug environment"+(a?": "+a:"."));};goog.forwardDeclare=function(a){};COMPILED||(goog.isProvided_=function(a){return a in goog.loadedModules_||!goog.implicitNamespaces_[a]&&null!=goog.getObjectByName(a)},goog.implicitNamespaces_={"goog.module":!0}); +goog.getObjectByName=function(a,b){a=a.split(".");b=b||goog.global;for(var c=0;c>>0);goog.uidCounter_=0;goog.cloneObject=function(a){var b=goog.typeOf(a);if("object"==b||"array"==b){if("function"===typeof a.clone)return a.clone();if("undefined"!==typeof Map&&a instanceof Map)return new Map(a);if("undefined"!==typeof Set&&a instanceof Set)return new Set(a);b="array"==b?[]:{};for(var c in a)b[c]=goog.cloneObject(a[c]);return b}return a};goog.bindNative_=function(a,b,c){return a.call.apply(a.bind,arguments)}; +goog.bindJs_=function(a,b,c){if(!a)throw Error();if(2").replace(/'/g,"'").replace(/"/g,'"').replace(/&/g,"&"));b&&(a=a.replace(/\{\$([^}]+)}/g,function(d,e){return null!=b&&e in b?b[e]:d}));return a};goog.getMsgWithFallback=function(a,b){return a};goog.exportSymbol=function(a,b,c){goog.exportPath_(a,b,!0,c)};goog.exportProperty=function(a,b,c){a[b]=c}; +goog.inherits=function(a,b){function c(){}c.prototype=b.prototype;a.superClass_=b.prototype;a.prototype=new c;a.prototype.constructor=a;a.base=function(d,e,f){for(var g=Array(arguments.length-2),h=2;h\x3c/script>';f+="";f=goog.Dependency.defer_?f+("document.getElementById('script-"+e+"').onload = function() {\n goog.Dependency.callback_('"+e+"', this);\n};\n"):f+("goog.Dependency.callback_('"+e+"', document.getElementById('script-"+e+"'));");f+="\x3c/script>";b.write(goog.TRUSTED_TYPES_POLICY_?goog.TRUSTED_TYPES_POLICY_.createHTML(f):f)}else{var g=b.createElement("script");g.defer=goog.Dependency.defer_;g.async=!1;c&&(g.nonce= +c);g.onload=function(){g.onload=null;a.loaded()};g.src=goog.TRUSTED_TYPES_POLICY_?goog.TRUSTED_TYPES_POLICY_.createScriptURL(this.path):this.path;b.head.appendChild(g)}}else goog.logToConsole_("Cannot use default debug loader outside of HTML documents."),"deps.js"==this.relativePath?(goog.logToConsole_("Consider setting CLOSURE_IMPORT_SCRIPT before loading base.js, or setting CLOSURE_NO_DEPS to true."),a.loaded()):a.pause()},goog.Es6ModuleDependency=function(a,b,c,d,e){goog.Dependency.call(this,a, +b,c,d,e)},goog.inherits(goog.Es6ModuleDependency,goog.Dependency),goog.Es6ModuleDependency.prototype.load=function(a){function b(k,l){var q="",p=goog.getScriptNonce_();p&&(q=' nonce="'+p+'"');k=l?' - - - - - -
-

Ruby mode

-
- - -

MIME types defined: text/x-ruby.

- -

Development of the CodeMirror Ruby mode was kindly sponsored - by Ubalo.

- -
diff --git a/src/components/editor/modes/csound/print-tree.ts b/src/components/editor/modes/csound/print-tree.ts deleted file mode 100644 index be5ef49e..00000000 --- a/src/components/editor/modes/csound/print-tree.ts +++ /dev/null @@ -1,293 +0,0 @@ -/* eslint-disable unicorn/no-abusive-eslint-disable */ -/* eslint-disable */ -// MIT License -// -// Copyright (c) 2021 Matthijs Steen -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -import { Text } from "@codemirror/text"; -import { Input, NodeType, SyntaxNode, Tree, TreeCursor } from "@lezer/common"; - -class StringInput implements Input { - constructor(private readonly input: string) {} - - get length() { - return this.input.length; - } - - chunk(from: number): string { - return this.input.slice(from); - } - - lineChunks = false; - - read(from: number, to: number): string { - return this.input.slice(from, to); - } -} - -export function sliceType( - cursor: TreeCursor, - input: Input, - type: number -): string | null { - if (cursor.type.id === type) { - const s = input.read(cursor.from, cursor.to); - cursor.nextSibling(); - return s; - } - return null; -} - -export function isType(cursor: TreeCursor, type: number): boolean { - const cond = cursor.type.id === type; - if (cond) cursor.nextSibling(); - return cond; -} - -export type CursorNode = { - type: NodeType; - from: number; - to: number; - isLeaf: boolean; -}; - -function cursorNode( - { type, from, to }: TreeCursor, - isLeaf = false -): CursorNode { - return { type, from, to, isLeaf }; -} - -export type TreeTraversal = { - beforeEnter?: (cursor: TreeCursor) => void; - onEnter: (node: CursorNode) => false | void; - onLeave?: (node: CursorNode) => false | void; -}; - -type TreeTraversalOptions = { - from?: number; - to?: number; - includeParents?: boolean; -} & TreeTraversal; - -export function traverseTree( - cursor: TreeCursor | Tree | SyntaxNode, - { - from = -Infinity, - to = Infinity, - includeParents = false, - beforeEnter, - onEnter, - onLeave - }: TreeTraversalOptions -): void { - if (!(cursor instanceof TreeCursor)) - cursor = (cursor instanceof Tree ? cursor.cursor() : cursor.cursor) as - | TreeCursor - | Tree - | SyntaxNode; - for (;;) { - let node = cursorNode(cursor as TreeCursor); - let leave = false; - if (node.from <= to && node.to >= from) { - const enter = - !node.type.isAnonymous && - (includeParents || (node.from >= from && node.to <= to)); - if (enter && beforeEnter) beforeEnter(cursor as TreeCursor); - node.isLeaf = !(cursor as TreeCursor).firstChild(); - if (enter) { - leave = true; - if (onEnter(node) === false) return; - } - if (!node.isLeaf) continue; - } - for (;;) { - node = cursorNode(cursor as TreeCursor, node.isLeaf); - if (leave && onLeave) if (onLeave(node) === false) return; - leave = cursor.type.isAnonymous; - node.isLeaf = false; - if ((cursor as TreeCursor).nextSibling()) break; - if (!(cursor as TreeCursor).parent()) return; - leave = true; - } - } -} - -function isChildOf(child: CursorNode, parent: CursorNode): boolean { - return ( - child.from >= parent.from && - child.from <= parent.to && - child.to <= parent.to && - child.to >= parent.from - ); -} - -export function validatorTraversal( - input: Input | string, - { fullMatch = true }: { fullMatch?: boolean } = {} -) { - if (typeof input === "string") input = new StringInput(input); - const state = { - valid: true, - parentNodes: [] as CursorNode[], - lastLeafTo: 0 - }; - return { - state, - traversal: { - onEnter(node) { - state.valid = true; - if (!node.isLeaf) state.parentNodes.unshift(node); - if (node.from > node.to || node.from < state.lastLeafTo) { - state.valid = false; - } else if (node.isLeaf) { - if ( - state.parentNodes.length && - !isChildOf(node, state.parentNodes[0]) - ) - state.valid = false; - state.lastLeafTo = node.to; - } else { - if (state.parentNodes.length) { - if (!isChildOf(node, state.parentNodes[0])) - state.valid = false; - } else if ( - fullMatch && - (node.from !== 0 || node.to !== input.length) - ) { - state.valid = false; - } - } - }, - onLeave(node) { - if (!node.isLeaf) state.parentNodes.shift(); - } - } as TreeTraversal - }; -} - -export function validateTree( - tree: TreeCursor | Tree | SyntaxNode, - input: Input | string, - options?: { fullMatch?: boolean } -): boolean { - const { state, traversal } = validatorTraversal(input, options); - traverseTree(tree, traversal); - return state.valid; -} - -enum Color { - Red = 31, - Green = 32, - Yellow = 33 -} - -function colorize(value: any, color: number): string { - return "\u001b[" + color + "m" + String(value) + "\u001b[39m"; -} - -type PrintTreeOptions = { - from?: number; - to?: number; - start?: number; - includeParents?: boolean; -}; - -export function printTree( - cursor: TreeCursor | Tree | SyntaxNode, - input: Input | string, - { from, to, start = 0, includeParents }: PrintTreeOptions = {} -): string { - const inp = typeof input === "string" ? new StringInput(input) : input; - const text = Text.of(inp.read(0, inp.length).split("\n")); - const state = { - output: "", - prefixes: [] as string[], - hasNextSibling: false - }; - const validator = validatorTraversal(inp); - traverseTree(cursor, { - from, - to, - includeParents, - beforeEnter(cursor) { - state.hasNextSibling = cursor.nextSibling() && cursor.prevSibling(); - }, - onEnter(node) { - validator.traversal.onEnter(node); - const isTop = state.output === ""; - const hasPrefix = !isTop || node.from > 0; - if (hasPrefix) { - state.output += (!isTop ? "\n" : "") + state.prefixes.join(""); - if (state.hasNextSibling) { - state.output += " ├─ "; - state.prefixes.push(" │ "); - } else { - state.output += " └─ "; - state.prefixes.push(" "); - } - } - const hasRange = node.from !== node.to; - state.output += - (node.type.isError || !validator.state.valid - ? colorize(node.type.name, Color.Red) - : node.type.name) + - " " + - (hasRange - ? "[" + - colorize(locAt(text, start + node.from), Color.Yellow) + - ".." + - colorize(locAt(text, start + node.to), Color.Yellow) + - "]" - : colorize(locAt(text, start + node.from), Color.Yellow)); - if (hasRange && node.isLeaf) { - state.output += - ": " + - colorize( - JSON.stringify(inp.read(node.from, node.to)), - Color.Green - ); - } - }, - onLeave(node) { - validator.traversal.onLeave!(node); - state.prefixes.pop(); - } - }); - return state.output; -} - -function locAt(text: Text, pos: number): string { - if (text.length < pos) { - return `${text.length}:0`; - } else { - const line = text.lineAt(pos); - return line.number + ":" + (pos - line.from); - } -} - -export function logTree( - tree: TreeCursor | Tree | SyntaxNode, - input: string, - options?: PrintTreeOptions -): void { - console.log(printTree(tree, input, options)); -} diff --git a/src/components/editor/modes/csound/syntax.grammar b/src/components/editor/modes/csound/syntax.grammar deleted file mode 100644 index bdae9df9..00000000 --- a/src/components/editor/modes/csound/syntax.grammar +++ /dev/null @@ -1,277 +0,0 @@ -@dialects { csd } - -@top Program { rootstatement } - -@skip { BlockComment | LineComment | space } - -@precedence { - call, - prefix, - plus @left, - arithOpL @left, - arithOpR @right, - gettr @right, - number, - opcodeargs @left, - ternary @left, - comma @left -} - -xmlStatement { - XmlOpen | XmlClose -} - -rootstatement { - (xmlStatement | statement | InstrumentDeclaration | UdoDeclaration | StructDeclaration | DeclareDeclaration)* -} - -statement { - AssignStatement | - OpcodeStatement | - GotoStatement | - ControlFlowElseStatement | - ControlFlowStatement | - newline -} - - -GotoStatement { - GotoLabel newline -} - - -InstrumentDeclaration { - kw<"instr"> commaSep<(FunctionName | Number)> newline - statement* - kw<"endin"> newline -} - -UdoDeclaration { - ( - kw<"opcode"> FunctionName "," OpcodeArguments "," OpcodeArguments newline | - kw<"opcode"> FunctionName "(" commaSep ")" ":" "(" commaSep ")" newline | - kw<"opcode"> FunctionName "(" commaSep ")" ":" SignalRateIdentifier newline - ) - statement* - kw<"endop"> newline -} - -StructDeclaration { - kw<"struct"> FunctionName commaSep newline -} - -DeclareDeclaration { - kw<"declare"> FunctionName "(" commaSep ")" ":" "(" commaSep ")" newline | - kw<"declare"> FunctionName "(" commaSep ")" ":" SignalRateIdentifier newline -} - -AssignStatement { - SignalRateIdentifier AssignOp expressionNoComma newline -} - -CallbackExpression { AmbiguousIdentifier !call ArgList newline? } - -expressionNoComma { - String | - AmbiguousIdentifier ("[" "]" )? (!gettr PropertyIdentifier ("[" expressionNoComma? "]" )? )? | - ParenthesizedExpression { "(" expressionNoComma ")" } | - BracketedExpression { AmbiguousIdentifier ("[" expressionNoComma "]")+ } | - !number Number | - UnaryExpression { - !plus ArithOp<"+" | "-"> expressionNoComma - } | - BinaryExpression { - expressionNoComma !arithOpR arithOpRight expressionNoComma | - expressionNoComma !arithOpL arithOpLeft expressionNoComma | - expressionNoComma !plus ArithOp<"+" | "-"> expressionNoComma - } | - CallbackExpression | - ConditionalExpression { - expressionNoComma !ternary "?" expressionNoComma ":" expressionNoComma - } -} - -ArgList { - "(" commaSep ")" | - "(" ")" -} - - -OpcodeStatement { - commaSep+ newline -} - -ConstrolFlowBeginToken { - kw<"if"> | kw<"while"> | kw<"until"> -} - -ControlFlowDoTokens { - kw<"do"> | ControlFlowDoToken -} - -ControlFlowEndTokens { - kw<"fi"> | ControlFlowEndToken -} - -ControlFlowStatement { - ConstrolFlowBeginToken expressionNoComma ControlFlowDoTokens newline - statement* - ControlFlowEndTokens | - ConstrolFlowBeginToken expressionNoComma ControlFlowGotoToken - DeclareGotoLabel { word } newline -} - -ControlFlowElseStatement { - ElseStatement { - ControlFlowElseIfToken expressionNoComma ControlFlowDoTokens | ControlFlowElseToken ControlFlowDoTokens - } -} - -FunctionName { word } - -SignalRateIdentifier { AmbiguousIdentifier } - -AssignOp { - kw<"init"> | assigners -} - -XmlAttribute { - XmlTagName "=" String -} - -XmlOpen { "<" AmbiguousIdentifier XmlAttribute* ">" } - -XmlClose { xmlCloseChars AmbiguousIdentifier ">" } - - -@tokens { - - LineComment { ";" ![\n]* | "//" ![\n]* } - - BlockComment { "/*" blockCommentRest } - - blockCommentRest { ![*] blockCommentRest | "*" blockCommentAfterStar } - - blockCommentAfterStar { "/" | "*" blockCommentAfterStar | ![/*] blockCommentRest } - - identifierChar { @asciiLetter | $[_$\u{a1}-\u{10ffff}] } - - Number { @digit+ | @digit? "." @digit+ | @digit+ "." @digit* } - - word { identifierChar (identifierChar | @digit)* } - - xmlCloseChars { "<" "/" } - - OpcodeArguments { ( $[aijkOPVJKopS0\[\]]+) } - - AmbiguousIdentifier { - ZeroDbFs | word (":" word)? - } - - PropertyIdentifier { "." AmbiguousIdentifier } - - XmlTagName { word } - - GotoLabel { - word ":" - } - - newline { $[\n\r]+ } - space { $[ \t]+ } - String { '"' (![\\\n"] | "\\" _)* '"'? } - ZeroDbFs { "0dbfs" } - - assigners { - "=" | - "+=" | - "-=" | - "*=" | - "/=" | - "|=" | - "&=" - } - - ArithOp { expr } - - arithOpRight { - "~" | - "!" | - "¬" - } - - arithOpLeft { - "/" | - "*" | - "%" | - "^" | - "#" | - "&" | - "|" | - "&&" | - "||" | - "<" | - ">" | - "<=" | - ">=" | - "==" | - "!=" | - ">>" | - "<<" - } - - @precedence { - space, - newline, - BlockComment, - LineComment, - xmlCloseChars, - arithOpLeft, - arithOpRight, - ControlFlowElseIfToken, - ControlFlowElseToken, - ControlFlowDoToken, - ControlFlowGotoToken, - ControlFlowEndToken, - PropertyIdentifier, - AmbiguousIdentifier, - GotoLabel, - assigners, - Number - } - - ControlFlowElseToken { "else" } - - ControlFlowElseIfToken { "elseif" } - - ControlFlowDoToken { - "then" | - "ithen" | - "kthen" - } - - ControlFlowGotoToken { - "goto" | - "igoto" | - "kgoto" - } - - ControlFlowEndToken { - "endif" | - "od" | - "enduntil" - } - -} - - -// Keywords - -kw { @specialize[@name={term}] } - - -commaSep { - content (!comma "," content?)* -} - - -@detectDelim diff --git a/src/components/editor/modes/csound/syntax.grammar.d.ts b/src/components/editor/modes/csound/syntax.grammar.d.ts deleted file mode 100644 index fbc79faf..00000000 --- a/src/components/editor/modes/csound/syntax.grammar.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { LRParser } from "@lezer/lr"; - -declare const parser: LRParser; -export { parser }; diff --git a/src/components/editor/modes/csound/tokens.js b/src/components/editor/modes/csound/tokens.js deleted file mode 100644 index b4c853df..00000000 --- a/src/components/editor/modes/csound/tokens.js +++ /dev/null @@ -1,20 +0,0 @@ -import { ContextTracker } from "@lezer/lr"; -import { BlockComment, LineComment, space, newline } from "./syntax.terms"; - -export const trackNewline = new ContextTracker({ - start: false, - shift(context, term) { - return term == LineComment || term == BlockComment || term == space - ? context - : term == newline; - }, - strict: false -}); - -// export const ignoreInlineComments = new ExternalTokenizer( -// (input, stack) => { -// // let { next } = input; -// console.log(input, stack); -// }, -// { contextual: true, fallback: true } -// ); diff --git a/src/components/editor/plugins/show-hint.js b/src/components/editor/plugins/show-hint.js index 40a532c5..6a51f134 100644 --- a/src/components/editor/plugins/show-hint.js +++ b/src/components/editor/plugins/show-hint.js @@ -80,11 +80,10 @@ function Completion(cm, options) { this.cm.getLine(this.startPos.line).length - this.cm.getSelection().length; - const self = this; cm.on( "cursorActivity", - (this.activityFunc = function () { - self.cursorActivity(); + (this.activityFunc = () => { + this.cursorActivity(); }) ); } @@ -152,9 +151,8 @@ Completion.prototype = { ) { this.close(); } else { - const self = this; - this.debounce = requestAnimationFrame(function () { - self.update(); + this.debounce = requestAnimationFrame(() => { + this.update(); }); if (this.widget) { this.widget.disable(); @@ -166,11 +164,11 @@ Completion.prototype = { if (!this.tick) { return; } - const self = this; const myTick = ++this.tick; - fetchHints(this.options.hint, this.cm, this.options, function (data) { - if (self.tick === myTick) { - self.finishUpdate(data, first); + + fetchHints(this.options.hint, this.cm, this.options, (data) => { + if (this.tick === myTick) { + this.finishUpdate(data, first); } }); }, @@ -321,8 +319,7 @@ function Widget(completion, data) { this.completion = completion; this.data = data; this.picked = false; - const widget = this, - cm = completion.cm; + const cm = completion.cm; const ownerDocument = cm.getInputField().ownerDocument; const parentWindow = ownerDocument.defaultView || ownerDocument.parentWindow; @@ -440,21 +437,21 @@ function Widget(completion, data) { cm.addKeyMap( (this.keyMap = buildKeyMap(completion, { - moveFocus: function (n, avoidWrap) { - widget.changeActive(widget.selectedHint + n, avoidWrap); + moveFocus: (n, avoidWrap) => { + this.changeActive(this.selectedHint + n, avoidWrap); }, - setFocus: function (n) { - widget.changeActive(n); + setFocus: (n) => { + this.changeActive(n); }, - menuSize: function () { - return widget.screenAmount(); + menuSize: () => { + return this.screenAmount(); }, length: completions.length, close: function () { completion.close(); }, - pick: function () { - widget.pick(); + pick: () => { + this.pick(); }, data: data })) @@ -502,20 +499,20 @@ function Widget(completion, data) { }) ); - CodeMirror.on(hints, "dblclick", function (event) { + CodeMirror.on(hints, "dblclick", (event) => { const t = getHintElement(hints, event.target || event.srcElement); if (t && t.hintId) { - widget.changeActive(t.hintId); - widget.pick(); + this.changeActive(t.hintId); + this.pick(); } }); - CodeMirror.on(hints, "click", function (event) { + CodeMirror.on(hints, "click", (event) => { const t = getHintElement(hints, event.target || event.srcElement); if (t && t.hintId) { - widget.changeActive(t.hintId); + this.changeActive(t.hintId); if (completion.options.completeOnSingleClick) { - widget.pick(); + this.pick(); } } }); @@ -555,10 +552,9 @@ Widget.prototype = { disable: function () { this.completion.cm.removeKeyMap(this.keyMap); - const widget = this; this.keyMap = { - Enter: function () { - widget.picked = true; + Enter: () => { + this.picked = true; } }; this.completion.cm.addKeyMap(this.keyMap); diff --git a/src/components/editor/utils.ts b/src/components/editor/utils.ts index 5f0b2c30..2ac096fb 100644 --- a/src/components/editor/utils.ts +++ b/src/components/editor/utils.ts @@ -49,6 +49,7 @@ export const evalBlinkExtension = StateField.define({ const findSurroundingContext = (view: EditorView, tree: TreeCursor) => { const treeRoot = tree.node; let maybeContext: any = treeRoot; + let lastContext: any = maybeContext; while (maybeContext) { if ( @@ -58,6 +59,17 @@ const findSurroundingContext = (view: EditorView, tree: TreeCursor) => { ) { return maybeContext; } + + // if we find ourselves in global scope, check if the user wanted to evaluate a global statement + if ( + maybeContext.type.name === "Program" && + ["OpcodeStatement", "CallbackExpression"].includes( + lastContext.type.name + ) + ) { + return lastContext; + } + lastContext = maybeContext; maybeContext = maybeContext.node.parent; } }; @@ -104,7 +116,7 @@ export const editorEvalCode = curry( view.state.selection.main.from !== view.state.selection.main.to; let selection; - let context; + let context: { from: number; to: number }; if (userHasSelection && !blockEval) { selection = view.state.sliceDoc( @@ -117,11 +129,22 @@ export const editorEvalCode = curry( ); context = findSurroundingContext(view, treeRoot); - if (context) { + + if ( + typeof context === "object" && + typeof context.from === "number" + ) { selection = view.state.sliceDoc(context.from, context.to); } } + // fallback to current line if no selection + if (!selection) { + const line = view.state.doc.lineAt(view.state.selection.main.head); + context = { from: line.from, to: line.to }; + selection = view.state.sliceDoc(line.from, line.to); + } + if (selection) { evalSelection({ csound, documentType, evalString: selection }).then( (result: number) => { diff --git a/src/components/file-tree/actions.ts b/src/components/file-tree/actions.ts index 8db2d772..0b9b65a6 100644 --- a/src/components/file-tree/actions.ts +++ b/src/components/file-tree/actions.ts @@ -1,19 +1,49 @@ +import { store } from "@root/store"; import { ADD_NON_CLOUD_FILE, CLEANUP_NON_CLOUD_FILES, - AddNonCloudFile, + DELETE_NON_CLOUD_FILE, NonCloudFile, - CleanupNonCloudFile + NonCloudFileTreeEntry, + CleanupNonCloudFileAction } from "./types"; +import { TAB_CLOSE } from "@comp/project-editor/types"; -export const addNonCloudFile = (file: NonCloudFile): AddNonCloudFile => { +export const nonCloudFiles: Map = new Map(); + +export const addNonCloudFile = ( + file: NonCloudFileTreeEntry +): { type: typeof ADD_NON_CLOUD_FILE; file: NonCloudFileTreeEntry } => { return { type: ADD_NON_CLOUD_FILE, - file + file: { + name: file.name, + createdAt: Number(file.createdAt) + } + }; +}; +export const deleteNonCloudFiles = (filename: string) => { + return { + type: DELETE_NON_CLOUD_FILE, + filename }; }; -export const cleanupNonCloudFiles = (): CleanupNonCloudFile => { +export const cleanupNonCloudFiles = ({ + projectUid +}: { + projectUid: string; +}): CleanupNonCloudFileAction => { + for (const openNcf of nonCloudFiles.keys()) { + store.dispatch({ + type: TAB_CLOSE, + projectUid, + documentUid: openNcf + }); + } + + nonCloudFiles.clear(); + return { type: CLEANUP_NON_CLOUD_FILES }; diff --git a/src/components/file-tree/context.tsx b/src/components/file-tree/context.tsx index 510b8528..d37b4190 100644 --- a/src/components/file-tree/context.tsx +++ b/src/components/file-tree/context.tsx @@ -32,7 +32,6 @@ const initialState: DnDState = { docIdx: {} }; export const DnDStateContext = createContext(initialState); export const DnDDispatchContext = createContext( - // eslint-disable-next-line @typescript-eslint/no-empty-function (dispatch: Record): void => {} ); @@ -129,17 +128,17 @@ const reducer = (state: DnDState, action: Record): DnDState => { ); const newState = reduceIndexed( - (accumulator, item, newIndex) => + (accumulator, item: any, newIndex) => assocPath( ["docIdx", item.uid, "index"], newIndex, accumulator ), - state, + state as DnDState, reorderedFiles ); - return newState; + return newState as DnDState; } case "setDocIdx": { return assoc("docIdx", action.docIdx, state); @@ -162,7 +161,7 @@ export const DnDProvider = ({ return ( + onDragEnd={(result: any) => dispatch({ type: "handleDrop", payload: result, project }) } > diff --git a/src/components/file-tree/header.tsx b/src/components/file-tree/header.tsx index e3709bbf..a7197004 100644 --- a/src/components/file-tree/header.tsx +++ b/src/components/file-tree/header.tsx @@ -1,9 +1,9 @@ import React from "react"; -import { useDispatch } from "react-redux"; +import { useDispatch } from "@root/store"; import { IProject } from "../projects/types"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faFolderPlus, faWindowClose } from "@fortawesome/free-solid-svg-icons"; -import Tooltip from "@material-ui/core/Tooltip"; +import CreateNewFolderIcon from "@mui/icons-material/CreateNewFolder"; +import DisabledByDefaultRoundedIcon from "@mui/icons-material/DisabledByDefaultRounded"; +import Tooltip from "@mui/material/Tooltip"; import { useTheme } from "@emotion/react"; import { windowHeader as windowHeaderStyle } from "@styles/_common"; import { setFileTreePanelOpen } from "@comp/project-editor/actions"; @@ -20,45 +20,43 @@ const FileTreeHeader = ({ const theme: any = useTheme(); const dispatch = useDispatch(); return ( - <> -
-

- {project ? project.name : ""} - - {isOwner && ( - - - dispatch(newFolder(project.projectUid)) - } - > - - - - )} - - +

+

+ {project ? project.name : ""} + + {isOwner && ( + - dispatch(setFileTreePanelOpen(false)) + dispatch(newFolder(project.projectUid)) } > - - -

-
- + )} + + + + dispatch(setFileTreePanelOpen(false)) + } + > + + + + +

+
); }; diff --git a/src/components/file-tree/index.tsx b/src/components/file-tree/index.tsx index a707b743..c618b0a6 100644 --- a/src/components/file-tree/index.tsx +++ b/src/components/file-tree/index.tsx @@ -1,55 +1,47 @@ import React, { useState, useCallback } from "react"; +import { AppThunkDispatch, useDispatch, useSelector } from "@root/store"; +import { getAuth } from "firebase/auth"; +import { uploadBytesResumable } from "firebase/storage"; +import { addDoc, collection, doc } from "firebase/firestore"; +import { v4 as uuidv4 } from "uuid"; import { - addIndex, - append, - assoc, - both, - concat, - curry, - find, - filter, - isEmpty, - last, - mergeAll, - not, - reduce, - reject, - sort, - path, - pathOr, - pipe, - propEq, - propOr, - values -} from "ramda"; -import { getType as mimeLookup } from "mime"; + getFirebaseTimestamp, + projects, + storageReference +} from "@config/firestore"; +import { curry, equals, path, propOr } from "ramda"; +import { Mime } from "mime"; import moment from "moment"; +import { openSnackbar } from "@comp/snackbar/actions"; +import { SnackbarType } from "@comp/snackbar/types"; import { rgba } from "@styles/utils"; -import { useTheme } from "@emotion/react"; +import { Theme, useTheme } from "@emotion/react"; import { Droppable, Draggable } from "react-beautiful-dnd"; -import { useDispatch, useSelector } from "react-redux"; -import Collapse from "@material-ui/core/Collapse"; -// import DescriptionIcon from "@material-ui/icons/Description"; -import InsertDriveFileIcon from "@material-ui/icons/InsertDriveFile"; +import Collapse from "@mui/material/Collapse"; +import Box from "@mui/material/Box"; +import InsertDriveFileIcon from "@mui/icons-material/InsertDriveFile"; import { CsdFileIcon, OrcFileIcon, ScoFileIcon, UdoFileIcon } from "@elem/filetype-icons"; -import EditIcon from "@material-ui/icons/EditTwoTone"; -import DeleteIcon from "@material-ui/icons/DeleteTwoTone"; +import EditIcon from "@mui/icons-material/EditTwoTone"; +import DeleteIcon from "@mui/icons-material/DeleteTwoTone"; import DownloadIcon from "@mui/icons-material/Download"; -import Tooltip from "@material-ui/core/Tooltip"; -import List from "@material-ui/core/List"; -import ListItem from "@material-ui/core/ListItem"; -import ListItemIcon from "@material-ui/core/ListItemIcon"; -import { ReactComponent as DirectoryClose } from "@root/svgs/fad-close.svg"; -import { ReactComponent as DirectoryOpen } from "@root/svgs/fad-open.svg"; -import { ReactComponent as WaveFormIcon } from "@root/svgs/fad-waveform.svg"; -import { IDocument, IDocumentsMap, IProject } from "../projects/types"; +import UploadIcon from "@mui/icons-material/Upload"; +import Tooltip from "@mui/material/Tooltip"; +import List from "@mui/material/List"; +import ListItem from "@mui/material/ListItem"; +import ListItemIcon from "@mui/material/ListItemIcon"; +import DirectoryClose from "@root/svgs/fad-close.svg?react"; +import DirectoryOpen from "@root/svgs/fad-open.svg?react"; +import WaveFormIcon from "@root/svgs/fad-waveform.svg?react"; +import { IDocument } from "../projects/types"; import { deleteFile, renameDocument } from "../projects/actions"; +import { textOrBinary } from "@comp/projects/utils"; import { + tabClose, tabOpenByDocumentUid, tabOpenNonCloudDocument } from "@comp/project-editor/actions"; @@ -57,13 +49,14 @@ import { selectIsOwner, selectCurrentTabDocumentUid } from "@comp/project-editor/selectors"; +import { nonCloudFiles, deleteNonCloudFiles } from "./actions"; import { useDnD } from "./context"; import * as SS from "./styles"; import FileTreeHeader from "./header"; import { selectNonCloudFiles } from "./selectors"; import { NonCloudFile } from "./types"; -const reduceIndexed = addIndex(reduce); +const mime = new Mime(); const RootReference = React.forwardRef((properties: any, reference: any) => (
@@ -82,8 +75,8 @@ const hopefulSorting = curry((documentIndex, documentA, documentB) => { return documentA.filename < documentB.filename ? -1 : documentA.filename > documentB.filename - ? 1 - : 0; + ? 1 + : 0; } }); @@ -103,15 +96,155 @@ const humanizeBytes = (size: number) => { } }; +function UploadNonCloudFileIcon({ + file, + projectUid, + mimeType +}: { + file: NonCloudFile; + projectUid: string; + mimeType: string; +}) { + const dispatch = useDispatch(); + const [uploadProgress, setUploadProgress] = useState(-1); + + const handleUpload = React.useCallback(() => { + const txtOrBin = textOrBinary(file.name); + const currentUser = getAuth().currentUser; + const uid = currentUser ? currentUser.uid : ""; + const documentId = uuidv4(); + + if (txtOrBin === "txt") { + const utf8decoder = new TextDecoder(); + const txt = utf8decoder.decode(file.buffer); + const document_ = { + type: txtOrBin, + name: file.name, + value: txt, + userUid: uid, + lastModified: getFirebaseTimestamp(), + created: getFirebaseTimestamp() + }; + + addDoc( + collection(doc(projects, projectUid), "files"), + document_ + ).then((result) => { + const documentUid = result.id; + dispatch(tabOpenByDocumentUid(documentUid, projectUid)); + }); + } else { + const metadata = { + contentType: + mimeType ?? + mimeType ?? + (txtOrBin ? "application/octet-stream" : "text/plain"), + customMetadata: { + filename: file.name, + projectUid, + userUid: uid, + docUid: documentId + } + }; + + storageReference(`${uid}/${projectUid}/${documentId}`).then( + (ref) => { + const uploadTask = uploadBytesResumable( + ref, + file.buffer, + metadata + ); + uploadTask.on( + "state_changed", + (snapshot) => { + const progress = + (snapshot.bytesTransferred / + snapshot.totalBytes) * + 100; + setUploadProgress(progress); + // console.log("Upload is " + progress + "% done"); + }, + (error) => { + console.error(error); + dispatch( + openSnackbar(error.message, SnackbarType.Error) + ); + }, + () => { + dispatch(tabClose(projectUid, file.name, false)); + dispatch(deleteNonCloudFiles(file.name)); + nonCloudFiles.delete(file.name); + setUploadProgress(-1); + dispatch( + openSnackbar( + "Upload done, the file should appear in a second...", + SnackbarType.Info + ) + ); + } + ); + } + ); + } + }, [dispatch, file, projectUid, setUploadProgress, mimeType]); + + return uploadProgress > -1 ? ( +
+
{`Upload: ${uploadProgress}%`}
+
+ ) : ( + + {`Upload ${propOr("", "name", file)} (${humanizeBytes( + file.buffer.length + )}) to your project`} +
{" "} + {`Created: ${moment( + file.createdAt + ).fromNow()}`} + + } + > + +
+ ); +} + function DownloadNonCloudFileIcon({ file, mimeType }: { file: NonCloudFile; mimeType: string; -}): JSX.Element { +}) { const onClick = useCallback(() => { - const blob = new Blob([file.buffer], { type: mimeType }); + const blob = new Blob([file.buffer as BlobPart], { type: mimeType }); const tmpUrl = URL.createObjectURL(blob); (window as any).open(tmpUrl); }, [file, mimeType]); @@ -144,14 +277,19 @@ function FileExtIcon({ isBinary: boolean; filename: string; nestingDepth?: number; -}): JSX.Element { +}) { if (isBinary) { return ( - + + + ); } @@ -184,7 +322,10 @@ function FileExtIcon({ ) : ( @@ -192,45 +333,57 @@ function FileExtIcon({ } const makeTree = ( - activeProjectUid, - currentTabDocumentUid, - dispatch, - collapseState, - setCollapseState, - isOwner, - theme, - path, - [documentIndex, elementArray], - filelist -) => { - const allDirectories = filter(propEq("type", "folder"), filelist); - const allFiles = filter((f) => not(propEq("type", "folder", f)), filelist); - const dragHoverCss = `{background-color: rgba(${rgba( - theme.allowed, - 0.1 - )}) !important;}`; + activeProjectUid: string, + currentTabDocumentUid: string, + dispatch: AppThunkDispatch, + collapseState: Record, + setCollapseState: (state: Record) => void, + isOwner: boolean, + theme: Theme, + path: string[], + [documentIndex]: [Record, any], + filelist: IDocument[] +): any[] => { + // Getting all directories (where type is "folder") + const allDirectories = filelist.filter((file) => file.type === "folder"); + + // Getting all files (where type is not "folder") + const allFiles = filelist.filter((file) => file.type !== "folder"); + + // const dragHoverCss = `{background-color: rgba(${rgba( + // theme.allowed, + // 0.1 + // )}) !important;}`; // this could be problematic, but then again, we need to test what behaviour we want - const sortedFiles = concat( - sort(hopefulSorting(documentIndex), allDirectories), - sort(hopefulSorting(documentIndex), allFiles) + // Sorting the files + const sortedFiles = [ + ...allDirectories.sort(hopefulSorting(documentIndex)), + ...allFiles.sort(hopefulSorting(documentIndex)) + ]; + + // Filtering current files based on path + const currentFiles = sortedFiles.filter((file) => + equals(file.path, path || []) ); - const currentFiles = filter(propEq("path", path || []), sortedFiles); - const newFileList = reject( - both(propEq("path", path || []), (p) => - not(propEq("type", "folder", p)) - ), - sortedFiles + // Creating a new file list by rejecting specific conditions + const newFileList = sortedFiles.filter( + (file) => !(file.path === (path || []) && file.type !== "folder") ); + // Finding the folder document based on the last path const folderDocument = - !isEmpty(path) && find(propEq("documentUid", last(path)), sortedFiles); + path && path.length > 0 + ? sortedFiles.find( + (file) => file.documentUid === path[path.length - 1] + ) + : undefined; const folderClassName = `folder-${ folderDocument ? folderDocument.documentUid : "root" }`; - const deleteIcon = (document_) => + const deleteIcon = (document_: IDocument) => isOwner && ( ); - const editIcon = (document_) => + const editIcon = (document_: IDocument) => isOwner && ( ); - return reduceIndexed( - ( - [documentIndex_, elementArray_], - document_: IDocument, - index: number - ) => { - if (propEq("type", "folder", document_)) { - const folderPath = append( - document_.documentUid, - document_.path - ); + const nextFiles = currentFiles.reduce( + (acc: any[], document_: IDocument, index: number) => { + const [documentIndex_, elementArray_] = acc; + + const commonDocumentData = { + index, + parent: folderDocument ? folderDocument.documentUid : "root" + }; + + if (document_.type === "folder") { + const folderPath = [...document_.path, document_.documentUid]; + const FolderIcon = ( {collapseState[document_.documentUid] ? ( - + + + ) : ( - + + + )} ); + const [newDocumentIndex, newElementArray] = makeTree( activeProjectUid, currentTabDocumentUid, @@ -301,274 +461,228 @@ const makeTree = ( isOwner, theme, folderPath, - [documentIndex_, []], + [documentIndex_, [] as any], newFileList ); - return [ - assoc( - document_.documentUid, - { - index, - parent: folderDocument - ? folderDocument.documentUid - : "root" - }, - newDocumentIndex - ), - pipe( - append( - - {(droppableProvided, droppableSnapshot) => ( - - - {(provided, snapshot) => ( - - setCollapseState( - assoc( - document_.documentUid, - not( - collapseState[ - document_ - .documentUid - ] - ), - collapseState - ) - ) - } - className={`folder-${document_.documentUid}`} - {...provided.draggableProps} - {...provided.dragHandleProps} - style={mergeAll([ - snapshot.isDragging - ? provided - .draggableProps - .style - : {}, - { - paddingLeft: 40, - height: 36 - } - ])} - button - > - {FolderIcon} - {document_.filename} -
- {deleteIcon(document_)} - {editIcon(document_)} -
-
- )} -
- + {(droppableProvided: any, droppableSnapshot: any) => ( + + + {(provided: any) => ( + + setCollapseState({ + ...collapseState, + [document_.documentUid]: + !collapseState[ + document_ + .documentUid + ] + }) } - key={`${document_.documentUid}-collapse`} + className={`folder-${document_.documentUid}`} + {...provided.draggableProps} + {...provided.dragHandleProps} + style={{ + ...provided.draggableProps + .style, + paddingLeft: 40, + height: 36 + }} > - {newElementArray} - - {droppableProvided.placeholder} - - -
- )} -
- ) - )(elementArray_) + {FolderIcon} + + {document_.filename} + +
+ {deleteIcon(document_)} + {editIcon(document_)} +
+ + )} + + + {newElementArray} + + {droppableProvided.placeholder} + + + + )} + + ); + + return [ + { + ...newDocumentIndex, + [document_.documentUid]: commonDocumentData + }, + [...elementArray_, folderElement] ]; } else { - return [ - assoc( - document_.documentUid, - { - index, - parent: folderDocument - ? folderDocument.documentUid - : "root" - }, - documentIndex_ - ), - append( - - {(droppableProvided, droppableSnapshot) => ( - - {droppableSnapshot.isDraggingOver && ( - - )} - - + {(droppableProvided: any) => ( + + + {(provided: any) => ( + + dispatch( + tabOpenByDocumentUid( + document_.documentUid, + activeProjectUid + ) + ) + } + sx={{ + paddingLeft: `${40 + 24 * path.length}px !important` + }} + style={{ + ...provided.draggableProps + .style, + height: 36, + backgroundColor: + currentTabDocumentUid === + document_.documentUid + ? "rgba(0,0,0,0.2)" + : "inherit" + }} > - {(provided, snapshot) => ( - - dispatch( - tabOpenByDocumentUid( - document_.documentUid, - activeProjectUid - ) - ) - } - style={mergeAll([ - snapshot.isDragging - ? mergeAll([ - provided - .draggableProps - .style, - { - backgroundColor: - "rgba(0, 0, 0, 0.1)" - } - ]) - : {}, - { - paddingLeft: - 40 + - 24 * - path.length, - height: 36, - backgroundColor: - currentTabDocumentUid === - document_.documentUid - ? "rgba(0,0,0,0.2)" - : "inherit" - } - ])} - button - > - - -

- {document_.filename} -

-
- {deleteIcon(document_)} - {editIcon(document_)} -
-
- )} -
-
- {droppableProvided.placeholder} -
- )} -
, - elementArray_ - ) + +

+ {document_.filename} +

+ + {deleteIcon(document_)} + {editIcon(document_)} + + + )} + + {droppableProvided.placeholder} + + )} + + ); + return [ + { + ...documentIndex_, + [document_.documentUid]: commonDocumentData + }, + [...elementArray_, fileElement as any] ]; } }, - [documentIndex, elementArray], - currentFiles + [{}, []] ); + return nextFiles; }; -const FileTree = (): React.ReactElement => { - const [collapseState, setCollapseState] = useState({}); - // const [isLoaded, setIsLoaded] = useState(false); +export const FileTree = ({ + activeProjectUid +}: { + activeProjectUid: string; +}) => { + const [collapseState, setCollapseState] = useState>( + {} + ); const [stateDnD] = useDnD(); const dispatch = useDispatch(); const theme = useTheme(); - const activeProjectUid: string = useSelector( - pathOr("", ["ProjectsReducer", "activeProjectUid"]) - ); - const nonCloudFiles = useSelector(selectNonCloudFiles); - - const isOwner: boolean = useSelector(selectIsOwner(activeProjectUid)); - const project: IProject | undefined = useSelector( - path(["ProjectsReducer", "projects", activeProjectUid]) + // Selectors + const nonCloudFileTreeEntries = useSelector(selectNonCloudFiles) || []; + const isOwner = useSelector(selectIsOwner); + const currentTabDocumentUid = useSelector(selectCurrentTabDocumentUid); + const project = useSelector( + (state) => state.ProjectsReducer.projects?.[activeProjectUid] ); + const documents = project?.documents || {}; - const documents: IDocumentsMap | undefined = useSelector( - path(["ProjectsReducer", "projects", activeProjectUid, "documents"]) - ); + // Extract file list and map non-cloud files + const filelist = Object.values(documents); - const currentTabDocumentUid = useSelector(selectCurrentTabDocumentUid); + const nonCloudFileSources = nonCloudFileTreeEntries + .map((entry) => nonCloudFiles.get(entry)) + .filter((file): file is NonCloudFile => !!file); + + const shouldDisplayTree = Boolean(stateDnD && project); - const filelist = values(documents || {}); + const [_, treeElements] = shouldDisplayTree + ? makeTree( + activeProjectUid, + currentTabDocumentUid || "", + dispatch, + collapseState, + setCollapseState, + isOwner, + theme, + [], + [stateDnD!.docIdx, []], + filelist + ) + : [{}, []]; return ( - {stateDnD && project && ( + {shouldDisplayTree && (
- { - makeTree( - activeProjectUid, - currentTabDocumentUid, - dispatch, - collapseState, - setCollapseState, - isOwner, - theme, - [], - [stateDnD.docIdx, []], - filelist - )[1] - } - {nonCloudFiles.length > 0 &&
} - {nonCloudFiles.map((file, index) => { - const mimeType = mimeLookup(file.name); + {treeElements} + {nonCloudFileSources.length > 0 &&
} + {nonCloudFileSources.map((file, index) => { + const mimeType = + mime.getType(file.name) || + "application/octet-stream"; return (
{ onClick={() => dispatch( tabOpenNonCloudDocument( - file.name + file.name, + mimeType ) ) } @@ -599,13 +714,29 @@ const FileTree = (): React.ReactElement => { )} nestingDepth={0} /> -

+

{file.name}

- + + + +
); @@ -617,5 +748,3 @@ const FileTree = (): React.ReactElement => { ); }; - -export default FileTree; diff --git a/src/components/file-tree/reducer.ts b/src/components/file-tree/reducer.ts index 4affdc1a..52abc594 100644 --- a/src/components/file-tree/reducer.ts +++ b/src/components/file-tree/reducer.ts @@ -1,40 +1,53 @@ -import { append, assoc, pipe } from "ramda"; +import { append, assoc, pipe, reject } from "ramda"; import { ADD_NON_CLOUD_FILE, CLEANUP_NON_CLOUD_FILES, - NonCloudFile, + DELETE_NON_CLOUD_FILE, + AddNonCloudFileAction, + DeleteNonCloudFileAction, FileTreeActionTypes } from "./types"; export interface IFileTreeReducer { - nonCloudFiles: NonCloudFile[]; + nonCloudFiles: string[]; } const INIT_STATE: IFileTreeReducer = { nonCloudFiles: [] }; const FileTreeReducer = ( state: IFileTreeReducer | undefined, - action: FileTreeActionTypes + unknownAction: FileTreeActionTypes ): IFileTreeReducer => { - if (!state) { - return INIT_STATE; - } else { - switch (action.type) { + if (state) { + switch (unknownAction.type) { case ADD_NON_CLOUD_FILE: { - return pipe( - assoc( - "nonCloudFiles", - append(action.file, state.nonCloudFiles) + const action = unknownAction as AddNonCloudFileAction; + return { + ...state, + nonCloudFiles: [...state.nonCloudFiles, action.file.name] + }; + } + case DELETE_NON_CLOUD_FILE: { + const action = unknownAction as DeleteNonCloudFileAction; + return { + ...state, + nonCloudFiles: state.nonCloudFiles.filter( + (filename) => filename !== action.filename ) - )(state); + }; } case CLEANUP_NON_CLOUD_FILES: { - return { nonCloudFiles: [] }; + return { + ...state, + nonCloudFiles: [] + }; } default: { return state || INIT_STATE; } } + } else { + return INIT_STATE; } }; diff --git a/src/components/file-tree/selectors.ts b/src/components/file-tree/selectors.ts index 5898f0ab..a12087ce 100644 --- a/src/components/file-tree/selectors.ts +++ b/src/components/file-tree/selectors.ts @@ -1,7 +1,5 @@ -import { pathOr } from "ramda"; -import { IStore } from "@store/types"; -import { NonCloudFile } from "./types"; +import { RootState } from "@root/store"; -export const selectNonCloudFiles = (store: IStore): NonCloudFile[] => { - return pathOr([] as any, ["FileTreeReducer", "nonCloudFiles"], store); +export const selectNonCloudFiles = (store: RootState): string[] => { + return store.FileTreeReducer.nonCloudFiles || []; }; diff --git a/src/components/file-tree/styles.tsx b/src/components/file-tree/styles.tsx index 117ed065..77b742d6 100644 --- a/src/components/file-tree/styles.tsx +++ b/src/components/file-tree/styles.tsx @@ -58,46 +58,45 @@ export const delEditContainer = css` export const headIconsContainer = (theme: Theme): SerializedStyles => css` position: absolute; - right: 16px; - + right: 18px; + margin-top: 2px; svg { - font-size: 16px; + font-size: 18px; + :hover { + fill: ${theme.textColor}!important; + } } height: 20px; & span { cursor: pointer; - &:hover { - svg { - color: ${theme.textColor}!important; - } - } } `; -export const listContainer = (theme: Theme): SerializedStyles => css` +export const listContainer = css` margin-top: -3px; `; -export const listItem = (theme: Theme): SerializedStyles => css` +export const listItem = css` padding-left: 32px; display: flex; justify-content: space-between; + cursor: pointer; `; export const draggingOver = (theme: Theme): SerializedStyles => css` - ${listItem(theme)} + ${listItem} & .MuiTouchRipple-root { background-color: rgba(${rgba(theme.allowed, 0.1)}) !important; } `; -export const listItemIcon = (theme: Theme): SerializedStyles => css` +export const listItemIcon = css` position: absolute; min-width: 18px; `; -export const listItemIconMui = (theme: Theme): SerializedStyles => css` - ${listItemIcon(theme)} +export const listItemIconMui = css` + ${listItemIcon} left: 12px; top: 4px; `; @@ -109,32 +108,31 @@ export const muiIcon = (theme: Theme): SerializedStyles => css` margin-left: 1px; `; -export const csoundFileIcon = (theme: Theme): SerializedStyles => css` +export const csoundFileIcon = css` svg { width: 28px; height: 28px; } `; -export const newFolderIcon = (theme: Theme): SerializedStyles => css` +export const newFolderIcon = css` margin-right: 12px; `; -const musicIconBase = (theme: Theme): SerializedStyles => css` +const musicIconBase = css` position: relative; width: 32px; height: 24px; `; export const mediaIcon = (theme: Theme): SerializedStyles => css` - ${musicIconBase(theme)} + ${musicIconBase} margin-top: 1px; - left: -36px; fill: ${theme.aRateVar}; `; export const directoryCloseIcon = (theme: Theme): SerializedStyles => css` - ${musicIconBase(theme)} + ${musicIconBase} width: 36px; height: 32px; margin-left: 2px; @@ -146,8 +144,8 @@ export const directoryCloseIcon = (theme: Theme): SerializedStyles => css` } `; -export const directoryOpenIcon = (theme: Theme): SerializedStyles => css` - ${musicIconBase(theme)} +export const directoryOpenIcon = css` + ${musicIconBase} width: 36px; height: 32px; `; @@ -159,4 +157,11 @@ export const filenameStyle = (theme: Theme): SerializedStyles => css` color: ${theme.textColor}; padding: 0; margin: 0; + margin-left: 12px; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + flex: 1; + min-width: 0; + max-width: calc(100% - 20px); `; diff --git a/src/components/file-tree/types.ts b/src/components/file-tree/types.ts index 0468ef7c..8b311d51 100644 --- a/src/components/file-tree/types.ts +++ b/src/components/file-tree/types.ts @@ -1,3 +1,5 @@ +import { UnknownAction } from "redux"; + export const ADD_NON_CLOUD_FILE = "FILE_TREE.ADD_NON_CLOUD_FILE"; export const DELETE_NON_CLOUD_FILE = "FILE_TREE.DELETE_NON_CLOUD_FILE"; export const CLEANUP_NON_CLOUD_FILES = "FILE_TREE.CLEANUP_NON_CLOUD_FILES"; @@ -9,20 +11,27 @@ export interface NonCloudFile { buffer: Uint8Array; } -export interface AddNonCloudFile { +export interface NonCloudFileTreeEntry { + createdAt: number; + name: string; +} + +export interface AddNonCloudFileAction { type: typeof ADD_NON_CLOUD_FILE; - file: NonCloudFile; + file: NonCloudFileTreeEntry; } -export interface CleanupNonCloudFile { +export interface CleanupNonCloudFileAction { type: typeof CLEANUP_NON_CLOUD_FILES; } -export interface DeleteNonCloudFile { +export interface DeleteNonCloudFileAction { type: typeof DELETE_NON_CLOUD_FILE; + filename: string; } export type FileTreeActionTypes = - | AddNonCloudFile - | CleanupNonCloudFile - | DeleteNonCloudFile; + | UnknownAction + | AddNonCloudFileAction + | CleanupNonCloudFileAction + | DeleteNonCloudFileAction; diff --git a/src/components/header/header.tsx b/src/components/header/header.tsx index d2cf67fa..a6174196 100644 --- a/src/components/header/header.tsx +++ b/src/components/header/header.tsx @@ -1,14 +1,16 @@ import React, { RefObject, useState, useRef } from "react"; -import { useDispatch, useSelector } from "react-redux"; -import { selectCurrentRoute } from "@comp/router/selectors"; +import { RootState, useDispatch, useSelector } from "@root/store"; import { selectIsOwner } from "@comp/project-editor/selectors"; -import { selectUserImageURL, selectUserName } from "@comp/profile/selectors"; +import { + selectUserImageURL, + selectLoggedInUserName +} from "@comp/profile/selectors"; import { selectLoggedInUid } from "@comp/login/selectors"; -import AppBar from "@material-ui/core/AppBar"; +import AppBar from "@mui/material/AppBar"; import Login from "@comp/login/login"; import * as loginActions from "@comp/login/actions"; import CSLogo from "@comp/cs-logo/cs-logo"; -import { Link } from "react-router-dom"; +import { Link, useLocation } from "react-router"; import { Toolbar, IconButton, @@ -16,68 +18,67 @@ import { Menu, Drawer, List, - ListItem, + ListItemButton, ListItemIcon, ListItemText, Divider -} from "@material-ui/core"; -import { AccountBox } from "@material-ui/icons"; -import Avatar from "@material-ui/core/Avatar"; -import Button from "@material-ui/core/Button"; -import MenuIcon from "@material-ui/icons/Menu"; -import HelpIcon from "@material-ui/icons/Help"; -import GitHubIcon from "@material-ui/icons/GitHub"; -import ReportProblemIcon from "@material-ui/icons/ReportProblem"; +} from "@mui/material"; +import { AccountBox } from "@mui/icons-material"; +import Button from "@mui/material/Button"; +import CachedAvatar from "@comp/profile/cached-avatar"; +import MenuIcon from "@mui/icons-material/Menu"; +import HelpIcon from "@mui/icons-material/Help"; +import GitHubIcon from "@mui/icons-material/GitHub"; +import ReportProblemIcon from "@mui/icons-material/ReportProblem"; import * as SS from "./styles"; // import { tooltipClasses } from "@comp/styles"; -import { IStore } from "@store/types"; import { isEmpty } from "ramda"; -import MenuBar from "@comp/menu-bar/menu-bar"; +import { MenuBar } from "@comp/menu-bar/menu-bar"; import ProjectProfileMeta from "./project-profile-meta"; -import TargetControls from "@comp/target-controls"; +import { TargetControls } from "@comp/target-controls"; import SocialControls from "@comp/social-controls/social-controls"; -const Header = (): React.ReactElement => { +export const Header = () => { const dispatch = useDispatch(); const authenticated = useSelector( - (store: IStore) => store.LoginReducer.authenticated + (store: RootState) => store.LoginReducer.authenticated ); const activeProjectUid = useSelector( - (store: IStore) => store.ProjectsReducer.activeProjectUid + (store: RootState) => store.ProjectsReducer.activeProjectUid ); - const currentRoute = useSelector(selectCurrentRoute); + const currentRoute = useLocation(); - const routeIsHome = currentRoute === "home"; + const routeIsHome = currentRoute.pathname === "/"; - const routeIsEditor = currentRoute === "editor"; + const routeIsEditor = currentRoute.pathname.startsWith("/editor"); - const routeIsProfile = currentRoute === "profile"; + const routeIsProfile = currentRoute.pathname.startsWith("/profile"); - const isOwner = useSelector(selectIsOwner(activeProjectUid || "")); + const isOwner = useSelector(selectIsOwner); const loggedInUid = useSelector(selectLoggedInUid); - const loggedInUserName = useSelector(selectUserName(loggedInUid)); + const loggedInUserName = useSelector(selectLoggedInUserName); const avatarUrl = useSelector(selectUserImageURL(loggedInUid || "")); const isLoginDialogOpen = useSelector( - (store: IStore) => store.LoginReducer.isLoginDialogOpen + (store: RootState) => store.LoginReducer.isLoginDialogOpen ); - const anchorElement = useRef() as RefObject; + const anchorElement: RefObject = useRef(null); const [isProfileMenuOpen, setIsProfileMenuOpen] = useState(false); const [isDrawerOpen, setIsDrawerOpen] = useState(false); - const handleProfileMenuOpen = (event?: any) => { + const handleProfileMenuOpen = () => { setIsProfileMenuOpen(true); }; - const handleProfileMenuClose = (event?: any) => { + const handleProfileMenuClose = () => { setIsProfileMenuOpen(false); }; @@ -87,7 +88,7 @@ const Header = (): React.ReactElement => { const avatar = isEmpty(avatarUrl) ? ( ) : ( - + ); const userMenu = () => ( @@ -119,16 +120,16 @@ const Header = (): React.ReactElement => { > { - handleProfileMenuClose(event); + onClick={() => { + handleProfileMenuClose(); }} > View Profile { - handleProfileMenuClose(event); + onClick={() => { + handleProfileMenuClose(); logout(); }} > @@ -182,8 +183,16 @@ const Header = (): React.ReactElement => { {routeIsEditor && !isOwner && }
- {routeIsEditor && } - {routeIsEditor && } + {routeIsEditor && activeProjectUid && ( + + )} + {routeIsEditor && activeProjectUid && ( + + )}
{authenticated ? userMenu() : loginButton()} @@ -196,8 +205,7 @@ const Header = (): React.ReactElement => {
- window.open("/documentation", "_blank") } @@ -206,9 +214,8 @@ const Header = (): React.ReactElement => { - - + window.open( "https://github.com/csound/web-ide/issues", @@ -220,12 +227,11 @@ const Header = (): React.ReactElement => { - + - window.open( "https://github.com/csound/web-ide", @@ -237,7 +243,7 @@ const Header = (): React.ReactElement => { - + {/* @@ -250,5 +256,3 @@ const Header = (): React.ReactElement => { ); }; - -export default Header; diff --git a/src/components/header/project-profile-meta.tsx b/src/components/header/project-profile-meta.tsx index ca4a58ee..378f64b5 100644 --- a/src/components/header/project-profile-meta.tsx +++ b/src/components/header/project-profile-meta.tsx @@ -1,44 +1,46 @@ import React from "react"; -import Moment from "react-moment"; import { useSelector } from "react-redux"; -import { Link } from "react-router-dom"; +import { Link } from "react-router"; +import { formatDistance } from "date-fns"; import { selectActiveProject } from "@comp/projects/selectors"; import { selectProjectLastModified } from "@comp/project-last-modified/selectors"; import { selectUserProfile, selectUserImageURL, - selectProfileProjectsCount + selectUserProjectsCount } from "@comp/profile/selectors"; import ProjectAvatar from "@elem/project-avatar"; -import { AccountBox } from "@material-ui/icons"; -import Avatar from "@material-ui/core/Avatar"; +import { AccountBox } from "@mui/icons-material"; +import Avatar from "@mui/material/Avatar"; import { IProject } from "@comp/projects/types"; -import Tooltip from "@material-ui/core/Tooltip"; +import Tooltip from "@mui/material/Tooltip"; import * as SS from "./styles"; -import { isEmpty, prop, propOr } from "ramda"; +import { isEmpty } from "ramda"; const ProjectProfileMeta = (): React.ReactElement => { + const now = new Date(); + const project: IProject | undefined = useSelector(selectActiveProject); - const projectName = propOr("unnamed", "name", project || {}); - const projectDescription = propOr("", "description", project || {}); + const projectName = project?.name ?? "unknown project name"; + const projectDescription = project?.description ?? ""; const projectLastModified = useSelector( - selectProjectLastModified(prop("projectUid", project || {})) + selectProjectLastModified(project?.projectUid) ); const projectLastModifiedDate = - projectLastModified && - projectLastModified.timestamp && - projectLastModified.timestamp.toDate && - projectLastModified.timestamp.toDate(); + projectLastModified && typeof projectLastModified.timestamp === "number" + ? new Date(projectLastModified?.timestamp) + : undefined; - const projectOwnerUid = propOr("", "userUid", project || {}); + const projectOwnerUid = project?.userUid ?? undefined; const profile = useSelector(selectUserProfile(projectOwnerUid)); - const profileUserName = propOr("", "username", profile || {}); - const profileDisplayName = propOr("", "displayName", profile || {}); + + const profileUserName = profile?.username ?? ""; + const profileDisplayName = profile?.displayName ?? "unknown user"; const profileImage = useSelector(selectUserImageURL(projectOwnerUid)); const profileProjectsCount = useSelector( - selectProfileProjectsCount(projectOwnerUid) + selectUserProjectsCount(projectOwnerUid) ); const authorTooltip = ( @@ -66,7 +68,15 @@ const ProjectProfileMeta = (): React.ReactElement => {
- +

@@ -83,13 +93,11 @@ const ProjectProfileMeta = (): React.ReactElement => { } > Modified - + {formatDistance( + projectLastModifiedDate, + now, + { addSuffix: true } + )} )}

diff --git a/src/components/header/styles.tsx b/src/components/header/styles.tsx index fd4bc1bc..3f0a270c 100644 --- a/src/components/header/styles.tsx +++ b/src/components/header/styles.tsx @@ -21,10 +21,11 @@ export const drawer = css` width: ${drawerWidth}; `; -export const drawerHeader = css` +export const drawerHeader = (theme: Theme): SerializedStyles => css` width: ${drawerWidth}; padding-left: 16px; height: 40px; + color: ${theme.textColor}; `; export const menuButton = css` @@ -32,7 +33,7 @@ export const menuButton = css` margin-right: 6px; `; -export const menuItemLink = (theme: Theme): SerializedStyles => css` +export const menuItemLink = css` text-decoration: none; `; diff --git a/src/components/home/actions.ts b/src/components/home/actions.ts index 2461ac0b..72b0f189 100644 --- a/src/components/home/actions.ts +++ b/src/components/home/actions.ts @@ -1,8 +1,15 @@ import { difference, keys, isEmpty, pluck } from "ramda"; -import { documentId, getDocs, query, where } from "firebase/firestore"; -import { profiles, projects } from "@config/firestore"; -import { Action } from "redux"; -import { ThunkAction } from "redux-thunk"; +import { getFunctions, httpsCallable } from "firebase/functions"; +import { + DocumentData, + documentId, + getDocs, + query, + Timestamp, + where +} from "firebase/firestore"; +import { profiles } from "@config/firestore"; +import { AppThunkDispatch, RootState } from "@root/store"; import { ADD_USER_PROFILES, ADD_RANDOM_PROJECTS, @@ -11,57 +18,88 @@ import { SEARCH_PROJECTS_SUCCESS, SET_POPULAR_PROJECTS_OFFSET, SET_RANDOM_PROJECTS_LOADING, - HomeActionTypes + HomeActionTypes, + RandomProjectResponse, + PopularProjectResponse } from "./types"; import { IProject } from "@comp/projects/types"; -import { - convertProjectSnapToProject, - firestoreProjectToIProject -} from "@comp/projects/utils"; -import { IStarredProjectSearchResult, IStarredProject } from "@db/search"; -import { IStore } from "@root/store/types"; - -const databaseID = - process.env.NODE_ENV === "development" || - process.env.REACT_APP_DATABASE === "DEV" - ? "dev" - : "prod"; -const searchURL = `https://web-ide-search-api.csound.com/search/${databaseID}`; +import { firestoreProjectToIProject } from "@comp/projects/utils"; +import { IProfile } from "../profile/types"; + +const functions = getFunctions(); +const getRandomProjects = httpsCallable< + { count: number }, + RandomProjectResponse[] +>(functions, "random_projects"); +const getPopularProjects = httpsCallable< + { count: number }, + PopularProjectResponse[] +>(functions, "popular_projects"); +const searchProjectsFunction = httpsCallable< + { + query: string; + offset?: number; + limit?: number; + sortBy?: "name" | "created" | "stars"; + sortOrder?: "asc" | "desc"; + }, + { + data: any[]; + totalRecords: number; + offset: number; + limit: number; + query: string; + } +>(functions, "search_projects"); + // const searchURL = `http://localhost:4000/search/${databaseID}`; export const searchProjects = - ( - query_: string, - offset: number - ): ThunkAction> => - async (dispatch) => { + (query_: string, offset: number) => async (dispatch: AppThunkDispatch) => { dispatch({ type: SEARCH_PROJECTS_REQUEST, query: query_, offset }); if (isEmpty(query_)) { return; } - const searchRequest = await fetch( - `${searchURL}/query/projects/${query_}/8/${offset}/name/desc` - ); - const projects = await searchRequest.json(); - projects.data = projects.data.slice(0, 8); + let projectsData: any[] = []; + let totalRecords = 0; + try { + const searchResponse = await searchProjectsFunction({ + query: query_, + offset, + limit: 8, + sortBy: "name", + sortOrder: "desc" + }); - const searchResult: IProject[] = projects.data.map( + projectsData = searchResponse.data.data; + totalRecords = searchResponse.data.totalRecords; + } catch (error) { + console.error(error); + } + + const searchResult: IProject[] = projectsData.map( firestoreProjectToIProject ); const userIDs = pluck("userUid", searchResult); if (!isEmpty(userIDs)) { - const projectProfiles = {}; + const projectProfiles: Record = {}; const profilesQuery = await getDocs( - (query as any)(profiles, where(documentId(), "in", userIDs)) + query(profiles, where(documentId(), "in", userIDs)) ); - profilesQuery.forEach((snapshot) => { + profilesQuery.forEach((snapshot: DocumentData) => { projectProfiles[snapshot.id] = snapshot.data(); + if (projectProfiles[snapshot.id]?.userJoinDate) { + projectProfiles[snapshot.id].userJoinDate = ( + projectProfiles[snapshot.id] + .userJoinDate as unknown as Timestamp + ).toMillis(); + } }); dispatch({ @@ -73,14 +111,14 @@ export const searchProjects = dispatch({ type: SEARCH_PROJECTS_SUCCESS, result: searchResult, - totalRecords: projects.totalRecords + totalRecords }); }; export const fetchPopularProjects = (offset = 0, pageSize = 8) => { return async ( - dispatch: (action: HomeActionTypes) => Promise, - getState: () => IStore + dispatch: AppThunkDispatch, + getState: () => RootState ): Promise => { const nextOffset = Math.max(0, offset) + pageSize; @@ -88,120 +126,109 @@ export const fetchPopularProjects = (offset = 0, pageSize = 8) => { type: SET_POPULAR_PROJECTS_OFFSET, newOffset: nextOffset }); - const state = getState().HomeReducer; - const starsRequest = await fetch( - `${searchURL}/list/stars/${pageSize}/${offset}/count/desc` - ); - const starredProjects: IStarredProjectSearchResult = - await starsRequest.json(); - - const starsIDs = starredProjects.data.map( - (item: IStarredProject) => item.id - ); + let popularProjects: PopularProjectResponse[] = []; + try { + // const starsRequest = await fetch( + // `${searchURL}/list/stars/${pageSize}/${offset}/count/desc` + // ); + // const starredProjects: IStarredProjectSearchResult = + // await starsRequest.json(); - if (!isEmpty(starsIDs)) { - const publicProjectsSnapshots = await getDocs( - query( - query(projects, where("public", "==", true)), - where(documentId(), "in", starsIDs) - ) - ); - const popularProjects: IProject[] = await Promise.all( - publicProjectsSnapshots.docs.map( - async (snap) => await convertProjectSnapToProject(snap) - ) - ); + const popularProjectsResponse = await getPopularProjects({ + count: pageSize + }); - const userIDs = pluck("userUid", popularProjects); + popularProjects = []; // popularProjectsResponse.data; + } catch (error) { + console.error(error); + } - const missingProfiles = difference(userIDs, keys(state.profiles)); - if (!isEmpty(missingProfiles)) { - const projectProfiles = {}; + const userIDs = popularProjects.map((project) => project.userUid); - const profilesQuery = await getDocs( - query(profiles, where(documentId(), "in", missingProfiles)) - ); + const missingProfiles = difference(userIDs, keys(state.profiles)); + if (!isEmpty(missingProfiles)) { + const projectProfiles: Record = {}; - profilesQuery.forEach((snapshot) => { - projectProfiles[snapshot.id] = snapshot.data(); - }); + const profilesQuery = await getDocs( + query(profiles, where(documentId(), "in", missingProfiles)) + ); - dispatch({ - type: ADD_USER_PROFILES, - payload: projectProfiles - }); - } + profilesQuery.forEach((snapshot: DocumentData) => { + projectProfiles[snapshot.id] = snapshot.data(); + if (projectProfiles[snapshot.id]?.userJoinDate) { + projectProfiles[snapshot.id].userJoinDate = ( + projectProfiles[snapshot.id] + .userJoinDate as unknown as Timestamp + ).toMillis(); + } + }); dispatch({ - type: ADD_POPULAR_PROJECTS, - payload: popularProjects, - totalRecords: starredProjects.totalRecords + type: ADD_USER_PROFILES, + payload: projectProfiles }); } + + dispatch({ + type: ADD_POPULAR_PROJECTS, + payload: popularProjects + }); }; }; export const fetchRandomProjects = () => { return async ( dispatch: (action: HomeActionTypes) => Promise, - getState: () => IStore + getState: () => RootState ): Promise => { dispatch({ type: SET_RANDOM_PROJECTS_LOADING, isLoading: true }); const state = getState().HomeReducer; - const randomProjectsRequest = await fetch( - `${searchURL}/random/projects/8` - ); + let randomProjects: RandomProjectResponse[] = []; - const randomProjects = await randomProjectsRequest.json(); + try { + const randomProjectsResponse = await getRandomProjects({ + count: 8 + }); + randomProjects = randomProjectsResponse.data; + } catch (error) { + console.error(error); + } - const randomProjectsIDs = randomProjects.data.map( - (item: IStarredProject) => item.id - ); + const userIDs = randomProjects.map((project) => project.userUid); + const missingProfiles = difference(userIDs, keys(state.profiles)); - if (!isEmpty(randomProjectsIDs)) { - const randomProjectsSnapshots = await getDocs( - query( - query(projects, where("public", "==", true)), - where(documentId(), "in", randomProjectsIDs) - ) - ); + if (!isEmpty(missingProfiles)) { + const projectProfiles: Record = {}; - const randomProjects: IProject[] = await Promise.all( - randomProjectsSnapshots.docs.map( - async (snap) => await convertProjectSnapToProject(snap) - ) + const profilesQuery = await getDocs( + query(profiles, where(documentId(), "in", missingProfiles)) ); - const userIDs = pluck("userUid", randomProjects); - - const missingProfiles = difference(userIDs, keys(state.profiles)); - - if (!isEmpty(missingProfiles)) { - const projectProfiles = {}; - - const profilesQuery = await getDocs( - query(profiles, where(documentId(), "in", missingProfiles)) - ); - - profilesQuery.forEach((snapshot) => { - projectProfiles[snapshot.id] = snapshot.data(); - }); - - dispatch({ - type: ADD_USER_PROFILES, - payload: projectProfiles - }); - } + profilesQuery.forEach((snapshot: DocumentData) => { + projectProfiles[snapshot.id] = snapshot.data(); + if (projectProfiles[snapshot.id]?.userJoinDate) { + projectProfiles[snapshot.id].userJoinDate = ( + projectProfiles[snapshot.id] + .userJoinDate as unknown as Timestamp + ).toMillis(); + } + }); dispatch({ - type: ADD_RANDOM_PROJECTS, - payload: randomProjects + type: ADD_USER_PROFILES, + payload: projectProfiles }); } + + dispatch({ + type: ADD_RANDOM_PROJECTS, + payload: randomProjects + }); + dispatch({ type: SET_RANDOM_PROJECTS_LOADING, isLoading: false }); }; }; diff --git a/src/components/home/background-style.js b/src/components/home/background-style.js deleted file mode 100644 index 1b8879a2..00000000 --- a/src/components/home/background-style.js +++ /dev/null @@ -1,18 +0,0 @@ -import { css } from "@emotion/react"; -import { headerHeight } from "@styles/constants"; - -export const homeBackground = css` - &:before { - content: " "; - width: 100vw; - height: 100vh; - top: 0; - left: 0; - position: fixed; - background-color: #3c3d37; - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='600' height='600' viewBox='0 0 600 600'%3E%3Cpath fill='%23272822' fill-opacity='0.4' d='M600 325.1v-1.17c-6.5 3.83-13.06 7.64-14.68 8.64-10.6 6.56-18.57 12.56-24.68 19.09-5.58 5.95-12.44 10.06-22.42 14.15-1.45.6-2.96 1.2-4.83 1.9l-4.75 1.82c-9.78 3.75-14.8 6.27-18.98 10.1-4.23 3.88-9.65 6.6-16.77 8.84-1.95.6-3.99 1.17-6.47 1.8l-6.14 1.53c-5.29 1.35-8.3 2.37-10.54 3.78-3.08 1.92-6.63 3.26-12.74 5.03a384.1 384.1 0 0 1-4.82 1.36c-2.04.58-3.6 1.04-5.17 1.52a110.03 110.03 0 0 0-11.2 4.05c-2.7 1.15-5.5 3.93-8.78 8.4a157.68 157.68 0 0 0-6.15 9.2c-5.75 9.07-7.58 11.74-10.24 14.51a50.97 50.97 0 0 1-4.6 4.22c-2.33 1.9-10.39 7.54-11.81 8.74a14.68 14.68 0 0 0-3.67 4.15c-1.24 2.3-1.9 4.57-2.78 8.87-2.17 10.61-3.52 14.81-8.2 22.1-4.07 6.33-6.8 9.88-9.83 12.99-.47.48-.95.96-1.5 1.48l-3.75 3.56c-1.67 1.6-3.18 3.12-4.86 4.9a42.44 42.44 0 0 0-9.89 16.94c-2.5 8.13-2.72 15.47-1.76 27.22.47 5.82.51 6.36.51 8.18 0 10.51.12 17.53.63 25.78.24 4.05.56 7.8.97 11.22h.9c-1.13-9.58-1.5-21.83-1.5-37 0-1.86-.04-2.4-.52-8.26-.94-11.63-.72-18.87 1.73-26.85a41.44 41.44 0 0 1 9.65-16.55c1.67-1.76 3.18-3.27 4.83-4.85.63-.6 3.13-2.96 3.75-3.57a71.6 71.6 0 0 0 1.52-1.5c3.09-3.16 5.86-6.76 9.96-13.15 4.77-7.42 6.15-11.71 8.34-22.44.86-4.21 1.5-6.4 2.68-8.6.68-1.25 1.79-2.48 3.43-3.86 1.38-1.15 9.43-6.8 11.8-8.72 1.71-1.4 3.26-2.81 4.7-4.3 2.72-2.85 4.56-5.54 10.36-14.67a156.9 156.9 0 0 1 6.1-9.15c3.2-4.33 5.9-7.01 8.37-8.07 3.5-1.5 7.06-2.77 11.1-4.02a233.84 233.84 0 0 1 7.6-2.2l2.38-.67c6.19-1.79 9.81-3.16 12.98-5.15 2.14-1.33 5.08-2.33 10.27-3.65l6.14-1.53c2.5-.63 4.55-1.2 6.52-1.82 7.24-2.27 12.79-5.06 17.15-9.05 4.05-3.72 9-6.2 18.66-9.9l4.75-1.82c1.87-.72 3.39-1.31 4.85-1.91 10.1-4.15 17.07-8.32 22.76-14.4 6.05-6.45 13.95-12.4 24.49-18.92 1.56-.96 7.82-4.6 14.15-8.33v-64.58c-4 8.15-8.52 14.85-12.7 17.9-2.51 1.82-5.38 4.02-9.04 6.92a1063.87 1063.87 0 0 0-6.23 4.98l-1.27 1.02a2309.25 2309.25 0 0 1-4.87 3.9c-7.55 6-12.9 10.05-17.61 13.19-3.1 2.06-3.86 2.78-8.06 7.13-5.84 6.07-11.72 8.62-29.15 10.95-11.3 1.5-20.04 4.91-30.75 11.07-1.65.94-7.27 4.27-6.97 4.1-2.7 1.58-4.69 2.69-6.64 3.66-5.63 2.8-10.47 4.17-15.71 4.17-17.13 0-41.44 11.51-51.63 22.83-12.05 13.4-31.42 27.7-45.25 31.16-7.4 1.85-11.85 7.05-14.04 14.69-1.26 4.4-1.58 8.28-1.58 13.82 0 .82.01.98.24 3.63.45 5.18.35 8.72-.77 13.26-1.53 6.2-4.89 12.6-10.59 19.43-13.87 16.65-22.88 46.58-22.88 71.68 0 2.39.02 4.26.06 8.75.12 10.8.1 15.8-.22 21.95-.56 11.18-2.09 20.73-5 29.3h-1.05c2.94-8.56 4.49-18.12 5.05-29.35.31-6.13.34-11.1.22-21.9-.04-4.48-.06-6.36-.06-8.75 0-25.32 9.07-55.47 23.12-72.32 5.6-6.72 8.88-12.99 10.38-19.03 1.09-4.4 1.18-7.85.74-12.93-.23-2.7-.24-2.86-.24-3.72 0-5.62.32-9.57 1.62-14.1 2.28-7.95 6.97-13.44 14.76-15.39 13.6-3.4 32.82-17.59 44.75-30.84C409 360.14 433.58 348.5 451 348.5c5.07 0 9.77-1.33 15.26-4.07 1.93-.96 3.9-2.05 6.58-3.62-.3.18 5.33-3.16 6.98-4.11 10.82-6.21 19.66-9.67 31.11-11.2 17.23-2.3 22.9-4.75 28.57-10.64 4.25-4.41 5.04-5.16 8.22-7.28 4.68-3.11 10.01-7.14 17.55-13.14a1113.33 1113.33 0 0 0 4.86-3.89l1.28-1.02a4668.54 4668.54 0 0 1 6.23-4.98c3.67-2.9 6.55-5.12 9.07-6.95 4.37-3.19 9.16-10.56 13.29-19.4v66.9zm0-116.23c-.62.01-1.27.06-1.95.13-6.13.63-13.83 3.45-21.83 7.45-3.64 1.82-8.46 2.67-14.17 2.71-4.7.04-9.72-.47-14.73-1.33-1.7-.3-3.26-.61-4.67-.93a31.55 31.55 0 0 0-3.55-.57 273.4 273.4 0 0 0-16.66-.88c-10.42-.16-17.2.74-17.97 2.73-.38.97.6 2.55 3.03 4.87 1.01.97 2.22 2.03 4.04 3.55a1746.07 1746.07 0 0 0 4.79 4.02c1.39 1.2 3.1 1.92 5.5 2.5.7.16.86.2 2.64.54 3.53.7 5.03 1.25 6.15 2.63 1.41 1.76 1.4 4.54-.15 8.88-2.44 6.83-5.72 10.05-10.19 10.33-3.63.23-7.6-1.29-14.52-5.06-4.53-2.47-6.82-7.3-8.32-15.26-.17-.87-.32-1.78-.5-2.86l-.43-2.76c-1.05-6.58-1.9-9.2-3.73-10.11-.81-.4-1.59-.74-2.36-1-2.27-.77-4.6-1.02-8.1-.92-2.29.07-14.7 1-13.77.93-20.55 1.37-28.8 5.05-37.09 14.99a133.07 133.07 0 0 0-4.25 5.44l-2.3 3.09-2.51 3.32c-4.1 5.36-7.06 8.48-10.39 11.12-.65.52-1.33 1.04-2.13 1.62l-4.11 2.94a106.8 106.8 0 0 0-5.16 3.99c-4.55 3.74-9.74 8.6-16.25 15.38-8.25 8.58-11.78 13.54-11.7 15.95.07 1.65 1.64 2.11 6.79 2.38 1.61.09 2.15.12 2.98.2 2.95.24 5.09.73 6.81 1.68 7.48 4.15 11.63 7.26 13.95 11.58 3.3 6.15.8 12.88-8.89 20.26-8.28 6.3-11.1 10.37-11.31 14.96-.06 1.17 0 1.93.26 4.43.69 6.47.25 10.65-2.8 17.42a44.23 44.23 0 0 1-4.16 7.53c-2.82 3.97-5.47 5.74-10.6 7.69-.43.16-3.34 1.23-4.27 1.59-1.8.68-3.38 1.36-5.01 2.14-4.18 2-8.4 4.6-13.1 8.24-8.44 6.51-13.23 14.56-15.98 25.06-1.1 4.2-1.55 6.81-2.8 15.21-1.26 8.6-2.17 12.64-4.08 16.55-2.1 4.28-11.93 26.59-12.97 28.88a382.7 382.7 0 0 1-6.37 13.41c-4.07 8.11-7.61 14.07-10.73 17.81-5.38 6.46-8.98 14.37-13.77 28.42a810.14 810.14 0 0 0-1.89 5.6c-1.8 5.35-2.96 8.6-4.26 11.85-6.13 15.32-25.43 26.31-46.46 26.31-11.2 0-20.58-2.74-31.02-8.55-5.6-3.13-4.55-2.42-22.26-14.54-14.33-9.8-17.7-10.73-20.47-6.9-.37.5-1.81 2.74-1.83 2.77a52.24 52.24 0 0 1-4.94 5.9c-.73.79-5.52 5.87-6.97 7.45-2.38 2.6-4.3 4.81-5.98 6.93a45.6 45.6 0 0 0-5.08 7.66c-1.29 2.57-1.9 5.25-2.66 10.6a997.6 997.6 0 0 1-.46 3.18h-1l.47-3.32c.77-5.45 1.4-8.2 2.75-10.9a46.54 46.54 0 0 1 5.2-7.84c1.7-2.14 3.63-4.38 6.03-6.98 1.45-1.59 6.24-6.68 6.96-7.46a51.58 51.58 0 0 0 4.84-5.78s1.47-2.26 1.86-2.8c3.25-4.5 7.08-3.44 21.84 6.67 17.67 12.08 16.62 11.38 22.19 14.48 10.3 5.73 19.5 8.43 30.53 8.43 20.65 0 39.57-10.77 45.54-25.69a219.7 219.7 0 0 0 4.24-11.8 6752.32 6752.32 0 0 0 1.88-5.6c4.83-14.16 8.47-22.14 13.96-28.73 3.05-3.66 6.56-9.57 10.6-17.61 1.97-3.93 4.04-8.31 6.35-13.38 1.03-2.28 10.88-24.61 12.98-28.91 1.85-3.79 2.75-7.76 4-16.25 1.24-8.44 1.7-11.07 2.81-15.32 2.8-10.7 7.71-18.94 16.33-25.6a73.18 73.18 0 0 1 13.29-8.35c1.66-.8 3.27-1.48 5.08-2.18.94-.36 3.86-1.43 4.28-1.59 4.95-1.88 7.44-3.55 10.14-7.33 1.35-1.9 2.68-4.3 4.06-7.37 2.97-6.58 3.39-10.59 2.72-16.9a27.13 27.13 0 0 1-.27-4.58c.22-4.94 3.21-9.24 11.7-15.7 9.33-7.11 11.66-13.34 8.62-19-2.2-4.09-6.25-7.12-13.55-11.17-1.57-.88-3.6-1.33-6.42-1.57-.8-.07-1.34-.1-2.95-.19-5.77-.3-7.63-.85-7.72-3.34-.1-2.81 3.5-7.87 11.97-16.69 6.53-6.8 11.75-11.69 16.33-15.45 1.79-1.47 3.42-2.72 5.2-4.03l4.12-2.94c.79-.58 1.46-1.08 2.1-1.59 3.26-2.6 6.16-5.65 10.21-10.94a383.2 383.2 0 0 0 2.5-3.32l2.31-3.09c1.8-2.39 3.04-4 4.29-5.48 8.47-10.17 16.98-13.96 37.27-15.3-.44.02 12-.9 14.32-.98 3.62-.1 6.05.16 8.46.98.8.27 1.62.62 2.47 1.04 2.27 1.14 3.17 3.87 4.27 10.85l.44 2.76c.17 1.07.33 1.97.5 2.83 1.44 7.69 3.62 12.29 7.8 14.57 6.76 3.68 10.6 5.15 13.99 4.94 4-.25 6.99-3.17 9.3-9.67 1.45-4.04 1.46-6.49.32-7.92-.9-1.12-2.28-1.62-5.57-2.27a55.8 55.8 0 0 1-2.67-.55c-2.54-.6-4.39-1.4-5.93-2.71a252.63 252.63 0 0 0-4.78-4.01 84.35 84.35 0 0 1-4.08-3.6c-2.73-2.6-3.86-4.43-3.28-5.95 1.02-2.64 7.82-3.54 18.93-3.37a230.56 230.56 0 0 1 16.73.88c2.76.39 3.2.49 3.68.6 1.4.3 2.95.62 4.62.91a82.9 82.9 0 0 0 14.56 1.32c5.56-.04 10.24-.86 13.73-2.6 8.1-4.05 15.89-6.9 22.17-7.56.7-.07 1.4-.11 2.05-.13v1zm0-100.94v1.5c-8.62 16.05-17.27 29.55-23.65 35.92-3.19 3.2-7.62 4.9-13.54 5.56-4.45.48-8.28.4-19.18-.2-9.91-.55-15.32-.44-20.52.78a84.05 84.05 0 0 1-15 2.11l-2.25.14c-12.49.75-19.37 1.78-32.72 5.74-4.5 1.33-9.27 2.49-14.3 3.48a246.27 246.27 0 0 1-32.6 3.97c-7.56.45-13.21.57-20.24.57-5.4 0-11.9 1.61-18 5.18-8.3 4.87-15.06 12.87-19.53 24.5a68.57 68.57 0 0 1-4.56 9.8c-3.6 6.2-6.92 8.99-13.38 12.18l-4.03 1.96a64.48 64.48 0 0 0-15.16 10.25c-8.2 7.33-13.72 16.63-22.54 35.6l-2.08 4.49c-7.3 15.7-11.5 23.3-17.35 29.87-7.7 8.66-20.25 14.42-40.31 20.08-4.37 1.23-19.04 5.08-19.24 5.13-6.92 1.87-11.68 3.34-15.63 4.92-10.55 4.22-18.71 10.52-36.38 26.52l-1.7 1.54c-8.58 7.76-13.41 11.9-18.81 15.88-3.95 2.9-8 5.67-12.97 8.91-2.06 1.34-10.3 6.6-12.33 7.94-11.52 7.5-18.53 13.04-24.62 20.08a62.01 62.01 0 0 0-6.44 8.85c-4.13 6.91-6.27 13.15-9.2 25.11l-1.54 6.26c-.6 2.45-1.15 4.54-1.72 6.58-2.97 10.7-6.9 17.36-14.78 26.91L69.6 491a148.51 148.51 0 0 0-4.19 5.3 23.9 23.9 0 0 0-3.44 6.28c-1.16 3.23-1.52 5.9-1.87 11.94-.58 10.05-1.42 15.04-4.63 22.67-1.57 3.72-5.66 14.02-6.41 15.8a73.46 73.46 0 0 1-3.57 7.4c-2.88 5.14-6.71 10.12-13.12 16.95-5.96 6.36-8.87 10.9-10.61 16a56.88 56.88 0 0 0-1.38 4.82l-.46 1.84h-1.03l.52-2.08c.52-2.09.92-3.49 1.4-4.9 1.8-5.25 4.78-9.9 10.84-16.36 6.35-6.78 10.13-11.7 12.97-16.77a72.5 72.5 0 0 0 3.52-7.29c.75-1.76 4.84-12.06 6.4-15.8 3.17-7.5 3.99-12.4 4.56-22.33.35-6.14.72-8.88 1.93-12.23a24.9 24.9 0 0 1 3.58-6.54c1.27-1.7 2.6-3.37 4.22-5.34l4.11-4.95c7.8-9.46 11.66-16 14.59-26.54.56-2.04 1.1-4.12 1.71-6.56l1.53-6.26c2.96-12.04 5.13-18.36 9.32-25.39 1.84-3.08 4-6.05 6.54-8.99 6.17-7.12 13.24-12.7 24.83-20.26 2.05-1.33 10.28-6.6 12.33-7.94 4.96-3.22 9-5.98 12.92-8.87 5.37-3.95 10.19-8.08 18.74-15.82l1.7-1.54c17.76-16.09 25.98-22.43 36.67-26.7 4-1.6 8.8-3.09 15.75-4.96.21-.06 14.87-3.9 19.22-5.13 19.9-5.61 32.32-11.31 39.85-19.78 5.76-6.48 9.93-14.02 17.18-29.64l2.09-4.5c8.87-19.07 14.44-28.46 22.77-35.9a65.48 65.48 0 0 1 15.38-10.4l4.04-1.97c6.3-3.1 9.47-5.77 12.96-11.77a67.6 67.6 0 0 0 4.48-9.67c4.56-11.84 11.47-20.02 19.97-25 6.25-3.66 12.93-5.32 18.5-5.32 7.01 0 12.65-.12 20.17-.57a245.3 245.3 0 0 0 32.47-3.96c5-.98 9.75-2.13 14.22-3.45 13.43-3.98 20.38-5.02 32.94-5.78l2.24-.14c5.76-.37 9.8-.9 14.85-2.09 5.31-1.25 10.79-1.35 22.6-.7 9.04.5 12.84.58 17.21.1 5.71-.62 9.94-2.26 12.95-5.26 6.44-6.45 15.3-20.37 24.35-36.72zm0 450.21c-1.28-4.6-2.2-10.55-3.33-20.25l-.24-2.04-.23-2.03c-1.82-15.7-3.07-21.98-5.55-24.47-2.46-2.46-3.04-5.03-2.52-8.64.1-.6.18-1.1.39-2.15.69-3.54.77-5.04.08-6.84-.91-2.38-3.31-4.41-7.79-6.26-5.08-2.09-6.52-4.84-4.89-8.44.66-1.45 1.79-3.02 3.52-5.01 1.04-1.2 5.48-5.96 5.08-5.53 6.15-6.7 8.98-11.34 8.98-16.48a15.2 15.2 0 0 1 6.5-12.89v1.26a14.17 14.17 0 0 0-5.5 11.63c0 5.47-2.93 10.29-9.24 17.16.38-.42-4.04 4.33-5.07 5.5-1.67 1.93-2.75 3.43-3.36 4.77-1.37 3.04-.23 5.22 4.36 7.1 4.71 1.95 7.32 4.16 8.34 6.83.78 2.04.7 3.67-.03 7.4-.2 1.03-.3 1.51-.38 2.09-.48 3.33.03 5.59 2.23 7.8 2.74 2.74 3.98 8.96 5.84 25.06l.24 2.03.23 2.04c.82 7.01 1.53 12.06 2.34 16.03v4.33zm0-62.16c-1.4-3.13-4.43-9.9-4.95-11.17-1.02-2.53-1.25-3.8-.91-5.18.2-.84 2.05-4.68 2.32-5.33a70.79 70.79 0 0 0 3.54-11.2v3.99a62.82 62.82 0 0 1-2.62 7.6c-.31.75-2.09 4.46-2.27 5.18-.28 1.12-.08 2.22.87 4.57.41 1.02 2.5 5.7 4.02 9.09v2.45zm0-85.09c-1.65 1.66-3.66 2.9-6.4 4.13-.25.1-13.97 5.47-20.4 8.43-9.35 4.32-16.7 5.9-23.03 5.25-5.08-.53-9.02-2.25-14.77-5.92l-3.2-2.07a77.4 77.4 0 0 0-5.44-3.27c-4.05-2.18-3.25-5.8 1.47-10.47 3.71-3.68 9.6-7.93 18.73-13.8l4.46-2.82c17.95-11.33 18.22-11.5 22.27-14.74 11.25-9 19.69-14.02 26.31-15.1v1.02c-6.37 1.1-14.62 6-25.69 14.86-4.1 3.28-4.34 3.44-22.36 14.8a652.4 652.4 0 0 0-4.45 2.83c-9.07 5.83-14.92 10.05-18.57 13.66-4.31 4.28-4.95 7.13-1.7 8.88 1.7.91 3.29 1.88 5.5 3.3l3.2 2.08c5.64 3.59 9.45 5.25 14.34 5.76 6.13.64 13.32-.9 22.52-5.15 6.46-2.98 20.18-8.35 20.4-8.44 3.04-1.37 5.1-2.71 6.81-4.69v1.47zm0-41.37v1c-6.56.26-12.11 3.13-19.71 9.08l-4.63 3.68a51.87 51.87 0 0 1-4.4 3.14c-.82.52-5.51 3.33-6.22 3.76-3.31 2-6.15 3.8-8.87 5.6a112.61 112.61 0 0 0-8.16 5.92c-4.61 3.72-7.4 6.9-7.97 9.35-.63 2.67 1.48 4.53 7.05 5.46 10.7 1.78 20.92-.05 30.45-4.65a61.96 61.96 0 0 0 17.1-12.2 41.8 41.8 0 0 0 5.36-7.42v1.92a38.94 38.94 0 0 1-4.64 6.19 62.95 62.95 0 0 1-17.39 12.41c-9.7 4.68-20.13 6.55-31.05 4.73-6.06-1-8.65-3.29-7.85-6.67.64-2.74 3.53-6.05 8.31-9.9 2.35-1.9 5.1-3.88 8.24-5.97 2.73-1.82 5.58-3.61 8.9-5.62.72-.44 5.4-3.24 6.22-3.75 1.26-.8 2.6-1.76 4.3-3.09.8-.62 3.9-3.1 4.63-3.67 7.77-6.1 13.49-9.04 20.33-9.3zm0-154.6v1c-1.75-.24-4.3.23-7.82 1.55-10.01 3.75-13.8 5.07-19.15 6.76-1.78.56-2.63.83-3.87 1.24-1.48.5-3.16.76-6.74 1.16a1550.34 1550.34 0 0 0-2.64.3c-7.8.94-11.28 2.47-11.28 6.07 0 4.45 2.89 13.18 7.96 25.81a57.34 57.34 0 0 1 2.33 7.6 258.32 258.32 0 0 1 .84 3.46c1.86 7.62 3.17 10.71 5.56 11.67 2.21.88 4.7.6 7.47-.72 3.48-1.69 7.22-4.94 11.2-9.47 1.52-1.7 2.97-3.49 4.59-5.57l3.16-4.1c2.59-3.23 6.07-12.21 8.39-20.23v3.45c-2.29 7.2-5.27 14.5-7.61 17.41-.44.55-2.67 3.46-3.15 4.09-1.63 2.1-3.1 3.9-4.62 5.62-4.08 4.61-7.9 7.94-11.53 9.7-2.99 1.44-5.77 1.75-8.28.74-2.84-1.13-4.2-4.34-6.15-12.35a2097.48 2097.48 0 0 1-.84-3.46c-.8-3.2-1.47-5.45-2.28-7.46-5.14-12.8-8.04-21.55-8.04-26.19 0-4.37 3.84-6.06 12.16-7.07a160.9 160.9 0 0 1 2.65-.3c3.5-.39 5.15-.64 6.53-1.1 1.26-.42 2.1-.7 3.88-1.26 5.34-1.68 9.11-3 19.1-6.74 3.53-1.32 6.22-1.84 8.18-1.61zM0 292c10.13-11.31 18.13-23.2 23.07-35.39 3.3-8.14 6.09-16.12 10.81-30.55l1.59-4.84c6.53-19.94 10.11-29.82 14.77-39.56 6.07-12.72 12.55-21.18 20.27-25.54 6.66-3.76 10.2-7.86 12.22-13.15a46.6 46.6 0 0 0 1.86-6.58c1.23-5.2 2.05-7.59 3.93-10.36 2.45-3.62 6.27-6.53 12.1-8.96 15.78-6.58 16.73-7.04 18.05-9.01.65-.98.83-2.15.74-4.51-.03-.73-.23-3.82-.24-4A93.8 93.8 0 0 1 119 94c0-10.04.18-11.37 2.37-13.15.52-.42 1.13-.8 2.07-1.3.27-.14 2.18-1.12 2.84-1.48a68.4 68.4 0 0 0 9.12-5.87c2.06-1.54 2.64-2.14 8.01-7.93 3.78-4.09 6.21-6.36 8.96-8.12 3.64-2.33 7.2-3.12 10.9-2.11 4.4 1.2 10.81 2 18.78 2.46 6.9.4 12.9.5 21.95.5 4.87 0 8.97.47 15.4 1.57 7.77 1.33 9.3 1.54 12.38 1.54 4.05 0 7.43-.88 10.68-2.95 5.06-3.22 8.11-4.67 11.2-5.2 3.62-.64 4.77-.46 16.55 2.06 17.26 3.7 30.85 1.36 41.06-9.7 5.1-5.53 5.48-8.9 3.48-14.8-.83-2.42-1.03-3.1-1.17-4.3-.29-2.52.5-4.71 2.71-6.93 2.65-2.65 4.72-9.17 6.22-18.29h2.03c-1.56 9.71-3.77 16.65-6.83 19.7-1.79 1.8-2.36 3.39-2.14 5.28.11 1 .3 1.63 1.07 3.9 2.22 6.53 1.76 10.66-3.9 16.8-10.77 11.66-25.07 14.13-42.95 10.3-11.42-2.45-12.55-2.62-15.78-2.06-2.77.48-5.62 1.84-10.47 4.92a20.93 20.93 0 0 1-11.76 3.27c-3.25 0-4.81-.22-12.73-1.57C212.74 59.46 208.73 59 204 59c-9.1 0-15.11-.1-22.07-.5-8.09-.47-14.62-1.29-19.2-2.54-5.62-1.53-10.17 1.38-17.85 9.66-5.5 5.94-6.08 6.53-8.28 8.18a70.38 70.38 0 0 1-9.38 6.03c-.68.37-2.58 1.35-2.84 1.49-.84.44-1.35.76-1.75 1.08C121.16 83.6 121 84.8 121 94c0 1.85.06 3.54.17 5.44 0 .17.2 3.28.24 4.03.1 2.75-.13 4.29-1.08 5.71-1.67 2.5-2.27 2.8-18.95 9.74-5.48 2.29-8.99 4.96-11.2 8.24-1.71 2.51-2.47 4.73-3.64 9.7-.83 3.5-1.21 4.92-1.94 6.83-2.18 5.73-6.05 10.19-13.1 14.18-7.3 4.12-13.55 12.28-19.46 24.66-4.6 9.64-8.17 19.46-14.67 39.32l-1.58 4.84c-4.75 14.47-7.54 22.48-10.86 30.69-5.28 13.01-13.95 25.65-24.93 37.6v-2.97zm0 78v-.5l1-.01c6.32 0 7.47 5.2 4.6 13.36a60.36 60.36 0 0 1-5.6 11.3v-1.92a57.76 57.76 0 0 0 4.65-9.72c2.69-7.6 1.71-12.02-3.65-12.02-.34 0-.67 0-1 .02v-46.59a340.96 340.96 0 0 0 13.71-8.34c13.66-9.46 29.79-37.6 29.79-53.59 0-18.1 21.57-72.64 32.23-79.42 12.71-8.09 32.24-27.96 35.8-37.75 1.93-5.3 5.5-7.27 14.42-9.37 6.15-1.44 8.64-2.42 10.67-4.79 1.5-1.74 2.72-4.79 4.33-10.3.23-.78 1.9-6.68 2.43-8.46 3.62-12.08 7.3-18.49 13.47-20.39 2.5-.76 3.03-.98 9.74-3.7 7.49-3.03 11.97-4.43 17.12-4.92 6.75-.65 13.13.75 19.55 4.67 5.43 3.32 12.19 4.72 20.17 4.56 6.03-.12 12.2-1.07 19.83-2.8 1.82-.4 7.38-1.74 8.26-1.94 2.69-.6 4.34-.89 5.48-.89 4.97 0 8.93-.05 14.2-.27 7.9-.32 15.56-.92 22.75-1.88 8.5-1.14 15.9-2.73 21.88-4.82 18.9-6.62 32.64-18.3 33.67-27.59.29-2.56.4-2.96 2.79-11.11 2.33-7.95 3.21-12.93 2.72-18.23-.2-2.24-.69-4.38-1.48-6.42-1.5-3.92-2.63-9.4-3.43-16.18h.9c.77 6.47 1.89 11.72 3.47 15.82a24.93 24.93 0 0 1 1.54 6.69c.5 5.46-.4 10.54-2.77 18.6-2.36 8.06-2.47 8.47-2.74 10.95-1.09 9.75-15.1 21.68-34.33 28.41-6.06 2.12-13.52 3.72-22.09 4.87-7.22.96-14.92 1.57-22.83 1.89-5.3.21-9.27.27-14.25.27-1.04 0-2.64.27-5.26.87-.87.2-6.43 1.53-8.26 1.94-7.68 1.73-13.92 2.7-20.03 2.82-8.15.17-15.1-1.27-20.71-4.7-6.23-3.81-12.4-5.16-18.93-4.54-5.04.48-9.44 1.86-16.84 4.86-6.75 2.74-7.29 2.95-9.82 3.73-5.73 1.76-9.28 7.96-12.81 19.72-.53 1.77-2.2 7.66-2.43 8.46-1.66 5.65-2.91 8.78-4.53 10.67-2.22 2.58-4.84 3.62-12.01 5.3-7.8 1.83-11.13 3.66-12.9 8.54-3.65 10.04-23.32 30.06-36.2 38.25C65.94 190 44.5 244.2 44.5 262c0 16.34-16.3 44.78-30.22 54.41-2.14 1.48-8.24 5.12-14.28 8.68v-1.16 46.09zm0-173.7v-1.11c7.42-3.82 14.55-10.23 21.84-18.98 3.8-4.56 14.21-18.78 15.79-20.55 1.8-2.04 4.06-3.96 7.42-6.45 1.08-.8 4.92-3.57 5.49-3.99 9.36-6.85 14-11.96 15.98-19.36.8-2.98 1.54-6.78 2.46-12.3.23-1.44 2-12.46 2.56-15.79 2.87-16.77 5.73-26.79 10.07-32.1C92.46 52.43 101.5 38.13 101.5 33c0-2.54.34-3.35 6.05-15.71.68-1.49 1.25-2.74 1.77-3.93 2.5-5.75 3.9-10.04 4.14-13.36h1c-.23 3.48-1.66 7.87-4.23 13.76-.52 1.2-1.09 2.45-1.78 3.95-5.54 12.01-5.95 12.99-5.95 15.29 0 5.47-9.09 19.84-20.11 33.31-4.2 5.12-7.03 15.06-9.86 31.64-.57 3.33-2.33 14.33-2.57 15.78-.92 5.56-1.67 9.38-2.48 12.4-2.05 7.68-6.82 12.93-16.35 19.91l-5.49 3.98c-3.3 2.45-5.51 4.34-7.27 6.31-1.53 1.73-11.94 15.93-15.76 20.53-7.52 9.02-14.88 15.6-22.61 19.46zm0 361.83v-4.33c.48 2.36 1 4.35 1.6 6.15 2 6.03 4.6 8.26 8.19 6.59C28.76 557.69 43.5 542.4 43.5 527c0-16.2 6.37-31.99 17.1-46.3 1.88-2.5 3.66-4.4 5.53-6 .73-.62 1.45-1.18 2.3-1.8l2-1.43c3.68-2.68 5.32-5.28 7.08-12.59.75-3.07 1.38-5.02 4.2-13.26l.63-1.88c3.24-9.58 4.56-14.97 4.17-18.65-.48-4.43-3.8-5.23-11.3-1.64a81.12 81.12 0 0 1-9.15 3.7c-13.89 4.67-26.96 5.8-42.66 5.42l-1.95-.05-1.45-.02a39.8 39.8 0 0 0-15.05 2.96A21.81 21.81 0 0 0 0 438.37v-1.26a23.55 23.55 0 0 1 4.55-2.57 40.77 40.77 0 0 1 16.92-3.02l1.95.05c15.6.38 28.57-.75 42.32-5.37a80.12 80.12 0 0 0 9.04-3.65c8.04-3.84 12.16-2.85 12.72 2.43.42 3.89-.92 9.34-4.21 19.08l-.64 1.88c-2.8 8.2-3.43 10.15-4.16 13.18-1.82 7.52-3.59 10.34-7.47 13.16l-2 1.43c-.84.6-1.54 1.15-2.25 1.75a35.45 35.45 0 0 0-5.37 5.84c-10.61 14.15-16.9 29.74-16.9 45.7 0 15.88-15 31.45-34.29 40.45-4.3 2.01-7.39-.66-9.56-7.18-.23-.68-.44-1.39-.65-2.13zm0-62.16v-2.45l1.46 3.27c2.1 4.8 3.46 10.33 4.26 16.77.66 5.3.84 9.3 1.04 18.5.2 9.32.5 12.75 1.63 15.05 1.28 2.6 3.67 2.35 8.29-1.5 17.14-14.3 21.82-22.9 21.82-38.62 0-7.17 1.1-12.39 3.7-17.68 2.27-4.67 3.65-6.62 13.4-19.62a69.8 69.8 0 0 1 7.6-8.79 44.76 44.76 0 0 1 3.54-3.06c.38-.3.64-.52.89-.74a10.47 10.47 0 0 0 2.63-3.32 35.78 35.78 0 0 0 2.26-5.94l.37-1.2.36-1.15c.29-.91.48-1.55.66-2.16.45-1.53.74-2.68.91-3.66.38-2.2.12-3.49-.85-4.15-2.35-1.61-9.28-.24-23.8 4.94-9.54 3.4-16.12 4.17-27.85 4.26-7.71.06-10.43.4-13.25 2.12-3.48 2.12-5.84 6.4-7.58 14.26-.5 2.2-.99 4.19-1.49 5.98v-3.98l.51-2.22c1.8-8.1 4.28-12.6 8.04-14.9 3.04-1.85 5.86-2.2 13.77-2.26 11.61-.09 18.1-.84 27.51-4.2 14.93-5.32 21.95-6.71 24.7-4.83 1.38.94 1.71 2.6 1.28 5.15a33.69 33.69 0 0 1-.94 3.78l-.66 2.17-.36 1.15-.37 1.2a36.64 36.64 0 0 1-2.33 6.1c-.8 1.53-1.61 2.52-2.86 3.61l-.92.77-1.02.83c-.9.74-1.65 1.4-2.47 2.18a68.84 68.84 0 0 0-7.48 8.66c-9.7 12.93-11.07 14.87-13.31 19.46-2.52 5.15-3.59 10.22-3.59 17.24 0 16.04-4.82 24.91-22.18 39.38-5.04 4.2-8.18 4.55-9.83 1.18-1.22-2.5-1.52-5.94-1.73-15.47-.2-9.16-.38-13.15-1.03-18.4-.79-6.34-2.12-11.8-4.19-16.49L0 495.98zM379.27 0h1.04l1.5 5.26c3.28 11.56 4.89 19.33 5.26 27.8.49 11.01-1.52 21.26-6.63 31.17-7.8 15.13-20.47 26.5-36.22 34.1-12.38 5.96-26.12 9.17-36.22 9.17-6.84 0-17.24 1.38-37.27 4.62l-2.27.37c-24.5 3.99-31.65 5-37.46 5-3.49 0-4.08-.08-19.54-2.8-3.56-.64-6.32-1.1-9-1.5-20.23-2.96-31-1.2-31.96 7.86-.1.85-.18 1.72-.29 2.81l-.27 2.73c-1.1 10.9-2.02 15.73-4.31 19.96-2.9 5.34-7.77 7.95-15.63 7.95-10.2 0-12.92.6-15.5 3.17.52-.51-5.03 5.85-8.16 8.7-2.75 2.5-14.32 12.55-15.77 13.83a341.27 341.27 0 0 0-6.54 5.92c-6.97 6.49-11.81 11.76-14.6 16.15-5.92 9.3-10.48 18.04-11.69 24.08-1.66 8.3 3.67 9.54 19.02 1.21a626.23 626.23 0 0 1 44.54-21.9c3.5-1.56 14.04-6.2 15.68-6.95 5.05-2.25 8.3-3.8 10.78-5.15l1.95-1.07 2.18-1.18c1.76-.94 3.38-1.76 5-2.55 18.1-8.72 34.48-10.46 50.33-1.2 22.89 13.34 38.28 37.02 38.28 56.44 0 19.12-.73 25.13-5.18 33.2a45.32 45.32 0 0 1-4.94 7.12c-6.47 7.77-11.81 16.2-12.76 21.27-1.2 6.34 4.69 7.03 20.17-.05 13.31-6.08 22.4-14.95 28.5-26.32a80.51 80.51 0 0 0 6.1-15.13c.9-2.98 3.17-11.65 3.41-12.48a29.02 29.02 0 0 1 1.75-4.83c7.47-14.93 21.09-30.5 36.25-37.24 7.61-3.38 13-9.65 19.4-20.79.84-1.48 4.26-7.64 5.14-9.17 3.52-6.1 6.22-9.7 9.37-11.98 10.15-7.4 28.7-11.1 50.29-11.1 7.52 0 16.54-1.24 27.51-3.58a420.1 420.1 0 0 0 14.96-3.52c-1.3.33 15.54-3.98 19.42-4.89 14.15-3.33 41.07-5.01 64.11-5.01 17.36 0 27.82-9.23 38.53-38.67 6.62-18.21 6.62-26.37 2.69-34.35l-1.18-2.37A13.36 13.36 0 0 1 587.5 58c0-4.03 0-4.01 2.5-24.56.46-3.73.8-6.74 1.12-9.64.9-8.45 1.38-15.2 1.38-20.8 0-.94-.02-1.94-.04-3h1c.03 1.06.04 2.06.04 3 0 5.65-.48 12.43-1.39 20.9-.3 2.91-.66 5.93-1.11 9.66-2.5 20.45-2.5 20.47-2.5 24.44 0 1.97.45 3.57 1.45 5.68.24.51 1.16 2.35 1.17 2.36 4.06 8.24 4.06 16.68-2.65 35.13-10.84 29.8-21.63 39.33-39.47 39.33-22.96 0-49.83 1.68-63.89 4.99-3.86.9-20.69 5.2-19.4 4.88a421.05 421.05 0 0 1-14.99 3.53c-11.04 2.35-20.11 3.6-27.72 3.6-21.4 0-39.76 3.67-49.7 10.9-3 2.19-5.64 5.7-9.1 11.68-.87 1.52-4.29 7.68-5.14 9.17-6.49 11.3-12 17.71-19.86 21.2-14.9 6.63-28.38 22.03-35.75 36.77a28.17 28.17 0 0 0-1.69 4.67c-.23.8-2.5 9.49-3.4 12.5a81.48 81.48 0 0 1-6.19 15.3c-6.2 11.56-15.44 20.58-28.96 26.76-16.1 7.36-23 6.55-21.58-1.04 1-5.29 6.4-13.83 12.99-21.73a44.33 44.33 0 0 0 4.82-6.96c4.35-7.88 5.06-13.77 5.06-32.72 0-19.04-15.19-42.4-37.72-55.55-15.57-9.08-31.62-7.38-49.45 1.21a132.9 132.9 0 0 0-7.14 3.71l-1.95 1.07a158.83 158.83 0 0 1-10.85 5.19c-1.65.74-12.18 5.38-15.69 6.95a625.25 625.25 0 0 0-44.46 21.86c-15.95 8.66-22.37 7.16-20.48-2.29 1.24-6.2 5.83-15.02 11.82-24.42 2.85-4.48 7.74-9.8 14.77-16.34 1.98-1.85 4.12-3.79 6.56-5.94 1.46-1.29 13.02-11.33 15.75-13.82 3.09-2.8 8.6-9.14 8.14-8.67 2.82-2.82 5.75-3.46 16.2-3.46 7.5 0 12.04-2.43 14.75-7.42 2.2-4.07 3.11-8.84 4.2-19.59l.26-2.73.3-2.81c.56-5.42 4.47-8.5 11.23-9.6 5.44-.88 12.51-.51 21.86.86 2.7.4 5.47.86 9.04 1.49 15.33 2.7 15.96 2.8 19.36 2.8 5.73 0 12.9-1.03 37.3-5l2.27-.36c20.1-3.26 30.52-4.64 37.43-4.64 9.95 0 23.54-3.18 35.78-9.08 15.57-7.5 28.09-18.73 35.78-33.65 5.02-9.75 7-19.82 6.51-30.67-.37-8.37-1.96-16.08-5.23-27.57L379.27 0zm13.68 0h1.02c.78 3.9 1.92 8.7 3.51 14.88 3.63 14.05 3.06 27.03-.75 38.77a61 61 0 0 1-11.35 20.68 138.36 138.36 0 0 1-19.32 18.77c-11.32 9.02-23.36 15.49-35.95 18.39a258.63 258.63 0 0 1-22.57 4.07c-3.17.44-6.36.85-10.3 1.32l-9.39 1.12c-11.53 1.41-17.45 2.55-21.64 4.46-9.28 4.21-28.35 6.04-49.21 6.04-1.37 0-2.8-.12-4.3-.35-2.62-.41-5-1.03-9.14-2.29-7.34-2.21-9.63-2.75-12.63-2.56-3.9.23-6.63 2.29-8.47 6.89-1.86 4.66-2.42 7.53-3.34 14.98-1.1 8.98-2.87 12.12-9.97 14.3a40.12 40.12 0 0 0-6.8 2.66c-.63.33-1.16.64-1.76 1.02l-1.34.86c-1.9 1.14-3.86 1.49-9.25 1.49-3.2 0-8.83-.55-9.51-.39-1.22.28-.75-.14-7.14 6.24-1.5 1.5-3.49 3.18-6.32 5.37-1.52 1.18-7.16 5.43-7.94 6.03-4.96 3.78-8.33 6.6-11.06 9.38-4.88 4.98-6.85 9.15-5.56 12.7 1.34 3.67 4.07 4.42 8.9 2.82a55.72 55.72 0 0 0 7.77-3.48c1.5-.77 7.78-4.13 9.37-4.96a116.8 116.8 0 0 1 12.31-5.68 162.2 162.2 0 0 0 11.04-4.84c2.04-.97 10.74-5.16 13-6.22 4.41-2.1 8.1-3.78 11.65-5.29 17.14-7.3 29.32-9.9 37.67-6.65l5.43 2.1c2.3.88 4.17 1.62 6.02 2.38a150.9 150.9 0 0 1 13.07 6c18.34 9.63 30.35 22.13 34.79 39.87 6.96 27.85 3.6 45.53-8.08 62.4-3.97 5.75-3.52 9.2.06 8.97 4.14-.28 10.21-4.95 15.11-12.52 3.1-4.8 5.1-10.45 8.05-21.53l1.69-6.35c.66-2.47 1.24-4.52 1.83-6.5 4.93-16.56 11-27.28 21.56-34.76 7.15-5.06 23.73-15.5 25.48-16.75 6.74-4.81 10.53-9.44 14.34-18 7.74-17.44 21.09-24.34 44.47-24.34 9.36 0 17.91-1.13 29.53-3.49a624.86 624.86 0 0 0 6.2-1.28c2.4-.5 4.07-.84 5.66-1.13 4.03-.74 7.04-1.1 9.61-1.1 4.44 0 9.39-1 31.39-5.99l2.95-.66c16.34-3.67 25.64-5.35 31.66-5.35 1.54 0 2.4.01 6.4.1 7.8.15 12.27.13 17.33-.2 16.41-1.06 26.73-5.36 29.8-14.56a87.1 87.1 0 0 1 3.55-8.83c-.15.31 2.29-4.96 2.9-6.38 5.38-12.3 5.57-21.92-1.44-39.44a86.4 86.4 0 0 1-5.26-20.72c-1.61-11.98-1.38-23.14.1-40.35l.2-2.12h1l-.2 2.2c-1.48 17.15-1.7 28.24-.11 40.14a85.4 85.4 0 0 0 5.2 20.47c7.1 17.78 6.91 27.67 1.43 40.22-.62 1.43-3.06 6.72-2.91 6.4a86.17 86.17 0 0 0-3.52 8.73c-3.23 9.72-13.9 14.15-30.68 15.24-5.1.33-9.58.35-17.42.2-3.98-.09-4.84-.1-6.37-.1-5.91 0-15.18 1.67-31.44 5.32l-2.95.67c-22.16 5.02-27.05 6.01-31.61 6.01-2.5 0-5.45.36-9.43 1.09-1.58.29-3.25.62-5.64 1.11a4894.21 4894.21 0 0 0-6.2 1.29c-11.68 2.37-20.3 3.51-29.73 3.51-23.02 0-36 6.71-43.53 23.66-3.9 8.8-7.82 13.58-14.7 18.5-1.78 1.27-18.36 11.7-25.48 16.75-10.34 7.32-16.3 17.87-21.19 34.23-.58 1.96-1.15 4-1.82 6.47l-1.69 6.35c-2.98 11.18-5 16.9-8.17 21.81-5.05 7.81-11.37 12.68-15.89 12.98-4.7.31-5.3-4.23-.94-10.53 11.52-16.64 14.82-34.03 7.92-61.6-4.35-17.42-16.16-29.72-34.27-39.22-4-2.1-8.2-4-12.99-5.97-1.84-.75-3.7-1.49-6-2.38l-5.43-2.08c-8.03-3.12-20.02-.58-36.92 6.63-3.52 1.5-7.21 3.19-11.61 5.27l-13 6.22c-4.71 2.22-8.16 3.75-11.11 4.88a115.87 115.87 0 0 0-12.21 5.63c-1.58.83-7.86 4.18-9.37 4.96a56.55 56.55 0 0 1-7.9 3.54c-5.3 1.75-8.62.85-10.17-3.43-1.46-4.02.66-8.5 5.8-13.74 2.75-2.82 6.16-5.66 11.15-9.48.79-.6 6.43-4.85 7.94-6.02a66.96 66.96 0 0 0 6.23-5.28c6.74-6.74 6.1-6.16 7.61-6.51.87-.2 6.69.36 9.74.36 5.22 0 7.03-.32 8.74-1.35l1.31-.84c.62-.4 1.18-.72 1.84-1.07a41.07 41.07 0 0 1 6.96-2.72c6.64-2.04 8.22-4.84 9.28-13.47.93-7.53 1.5-10.47 3.4-15.24 1.99-4.95 5.04-7.26 9.34-7.51 3.17-.2 5.5.35 12.97 2.6a63.54 63.54 0 0 0 9.02 2.26c1.45.22 2.83.34 4.14.34 20.71 0 39.7-1.82 48.8-5.96 4.32-1.96 10.29-3.1 21.93-4.53l9.4-1.12c3.92-.48 7.11-.88 10.27-1.32 8.16-1.14 15.4-2.43 22.49-4.06 12.42-2.86 24.33-9.26 35.55-18.2a137.4 137.4 0 0 0 19.18-18.64 60.02 60.02 0 0 0 11.15-20.32c3.76-11.57 4.32-24.36.75-38.23A284.86 284.86 0 0 1 392.95 0zM506.7 0h1.26c-.5.66-.9 1.18-1.17 1.51-3.95 4.96-6.9 7.92-9.82 9.57A10.02 10.02 0 0 1 492 12.5c-2.38 0-4.24.67-6.71 2.21l-2.65 1.71c-4.38 2.8-8.01 4.08-13.64 4.08-5.6 0-9.99-1.26-16.08-4.05a202.63 202.63 0 0 1-2.3-1.06l-2.18-.98c-1.6-.7-2.92-1.17-4.17-1.48a13.42 13.42 0 0 0-3.27-.43c-2.3 0-4.3-.68-11-3.37l-1.56-.62c-5-1.97-8.1-2.82-10.52-2.66-2.93.2-4.42 2.03-4.42 6.15 0 20.76-5.21 50.42-12.15 57.35-7.58 7.59-26.55 23.7-34.06 29.06-13.16 9.4-31.17 20.2-44.11 25.06a106.87 106.87 0 0 1-13.32 4.03c-3.28.78-6.6 1.43-11.25 2.24-.53.1-8.8 1.5-11.5 1.99-4.86.87-9.3 1.74-14 2.76-20.62 4.48-25.07 5.01-38.11 5.01-2.49 0-2.9-.07-14.05-2-2.42-.42-4.31-.73-6.15-1-8.11-1.19-13.83-1.36-17.64-.2-4.54 1.4-5.93 4.65-3.7 10.52 2.02 5.28 4.84 8.61 8.84 10.74 3.26 1.74 6.75 2.6 13.82 3.71 9.42 1.48 10.94 1.75 15.5 2.92a78.2 78.2 0 0 1 18.62 7.37c8.3 4.58 14.58 11.5 19.98 20.89 2.73 4.73 9.46 19.33 10.54 21.19 3.4 5.85 6.26 6.63 10.89 2 4.95-4.94 10.35-8.37 21.13-14.06.47-.25 2.06-1.1 2.12-1.12 7.98-4.21 11.92-6.51 15.87-9.54 5.11-3.9 8.66-8.1 10.77-13.11 8.52-20.24 20.75-33.31 32.46-33.31l5.5.03c10.53.08 17.35.02 24.9-.31 13.66-.62 23.78-2.09 29.39-4.67 5.85-2.7 13.42-5.49 24.18-9.02 3.46-1.14 6.29-2.05 12.7-4.1 7.7-2.45 11.08-3.54 15.17-4.9a1059.43 1059.43 0 0 1 11.33-3.72c3.67-1.2 5.96-2 8.03-2.78a59.88 59.88 0 0 0 6.66-2.94c1.87-.98 3.76-2.1 5.86-3.5 3.48-2.33 6.15-3.13 12.04-4.13l1.15-.2c5.71-1.01 9-2.3 12.76-5.63 7.82-6.96 8.58-23.18 3.84-44.52-1.7-7.67-2.1-19.28-1.57-35.47A837.22 837.22 0 0 1 546.76 0h1l-.15 3.06c-.32 6.42-.53 11.02-.68 15.62-.51 16.1-.12 27.65 1.56 35.21 4.82 21.68 4.04 38.2-4.16 45.48-3.91 3.48-7.37 4.84-13.24 5.87l-1.16.2c-5.76.99-8.32 1.75-11.65 3.98a63.73 63.73 0 0 1-5.96 3.56 60.86 60.86 0 0 1-6.77 2.99c-2.09.79-4.39 1.58-8.07 2.79a5398.31 5398.31 0 0 1-11.32 3.71c-4.1 1.37-7.48 2.46-15.18 4.92-6.42 2.04-9.24 2.95-12.7 4.08-10.73 3.53-18.27 6.3-24.07 8.98-5.76 2.66-15.97 4.14-29.77 4.77-7.56.33-14.4.39-24.95.31l-5.49-.03c-11.19 0-23.16 12.79-31.54 32.7-2.19 5.19-5.84 9.52-11.08 13.52-4.02 3.07-7.99 5.39-16.01 9.62l-2.12 1.12c-10.7 5.65-16.04 9.04-20.9 13.9-5.14 5.14-8.75 4.15-12.45-2.22-1.12-1.92-7.85-16.5-10.54-21.2-5.33-9.24-11.48-16.02-19.6-20.5a77.2 77.2 0 0 0-18.4-7.28c-4.5-1.17-6.02-1.43-15.4-2.9-7.17-1.12-10.74-2-14.13-3.81-4.22-2.25-7.2-5.77-9.3-11.27-2.43-6.39-.78-10.26 4.34-11.83 4-1.22 9.82-1.05 18.08.17 1.84.27 3.74.58 6.17 1 11.02 1.9 11.48 1.98 13.88 1.98 12.96 0 17.35-.52 37.9-4.99 4.71-1.02 9.16-1.9 14.03-2.77 2.71-.48 10.98-1.9 11.5-1.98 4.64-.81 7.95-1.46 11.2-2.23 4.55-1.07 8.76-2.34 13.2-4 12.83-4.81 30.79-15.59 43.88-24.94 7.47-5.33 26.4-21.4 33.94-28.94C407.3 61.98 412.5 32.49 412.5 12c0-4.61 1.86-6.9 5.35-7.15 2.63-.18 5.8.7 10.96 2.73l1.56.62c6.53 2.62 8.53 3.3 10.63 3.3 1.14 0 2.3.16 3.5.46 1.32.33 2.68.82 4.34 1.53a90.97 90.97 0 0 1 3.34 1.52l1.15.54c5.98 2.73 10.23 3.95 15.67 3.95 5.41 0 8.87-1.21 13.1-3.92.2-.13 2.1-1.38 2.66-1.72 2.62-1.63 4.64-2.36 7.24-2.36 1.47 0 2.94-.43 4.47-1.3 2.78-1.56 5.67-4.45 9.54-9.31l.7-.89zM324.54 600h-2.03c.49-2.96.91-6.2 1.28-9.66.44-4.1.76-8.25.98-12.21.08-1.39.14-2.65-.35-7.29-.47-1.94-.93-4.14-1.36-6.54-2.01-11.26-2.66-22.9-1.14-33.78a60.76 60.76 0 0 1 5.18-17.95 70.78 70.78 0 0 1 12.6-18.22c3.38-3.6 5.53-5.5 11.83-10.79 4.5-3.78 6.35-5.56 7.52-7.5.64-1.07.95-2.06.95-3.06 0-1.75 0-1.74-.75-9.23-.36-3.7-.57-6.3-.68-8.96-.5-12.1 1.62-19.6 8.11-21.76 15.9-5.3 25.89-12.1 33.45-25.54C409.6 390.65 425.85 376 436 376c12.36 0 20-1.96 29.41-8.8 6.76-4.92 9.5-6.6 12.47-7.46 2.22-.64 3.8-.74 9.12-.74 1.86 0 3.53-.83 5.57-2.62 1.08-.96 5.11-5.12 5.6-5.6 6.04-5.85 11.98-8.78 20.83-8.78 2.45 0 4.54.04 7.32.12 7.51.23 8.87.17 11.27-.7 3.03-1.1 5.53-3.03 14.75-11.17 8-7.06 10.72-8.92 22.87-16.47 1.44-.9 2.59-1.63 3.69-2.37a69.45 69.45 0 0 0 9.46-7.5c4.12-3.88 8.02-7.85 11.64-11.9v2.98a201.58 201.58 0 0 1-10.27 10.38c-3.18 3-6.2 5.35-9.72 7.7-1.12.76-2.28 1.5-3.75 2.4-12.05 7.5-14.71 9.32-22.6 16.28-9.46 8.35-12.01 10.32-15.39 11.55-2.74 1-4.19 1.06-12.01.82-2.76-.08-4.83-.12-7.26-.12-8.27 0-13.75 2.7-19.43 8.22-.44.43-4.52 4.64-5.68 5.66-2.37 2.09-4.46 3.12-6.89 3.12-5.1 0-6.6.1-8.56.66-2.67.78-5.29 2.37-11.85 7.15-9.8 7.13-17.85 9.19-30.59 9.19-9.22 0-24.96 14.2-34.13 30.49-7.84 13.94-18.24 21.02-34.55 26.46-5.31 1.77-7.21 8.51-6.75 19.78.1 2.6.31 5.19.68 8.84.75 7.62.75 7.58.75 9.43 0 1.38-.42 2.73-1.24 4.09-1.33 2.2-3.26 4.07-7.94 8-6.25 5.24-8.36 7.12-11.67 10.63a68.8 68.8 0 0 0-12.25 17.71 58.8 58.8 0 0 0-5 17.36c-1.49 10.66-.85 22.09 1.13 33.15.43 2.37.88 4.53 1.33 6.44.16.66.3 1.25.6 4.06a249.3 249.3 0 0 1-1.17 16.12c-.37 3.37-.78 6.53-1.25 9.44zm-13.4 0h-1.05l.12-.28c3.07-7.16 4.29-11.83 4.29-18.72 0-3.57-.07-4.93-.76-15.65-.77-12.04-1-19.64-.55-28.3.58-11.5 2.4-22.1 5.81-32.16 1.3-3.8 2.8-7.5 4.55-11.1 3.46-7.14 6.83-12.39 10.42-16.6a59.02 59.02 0 0 1 4.35-4.56c.43-.4 3-2.8 3.67-3.45 5.72-5.6 7.51-11.52 7.51-29.18 0-18.84 2.9-23.77 15.82-28.24 1.09-.37 1.92-.67 2.77-.98a51.3 51.3 0 0 0 6.1-2.7c4.95-2.6 9.64-6.22 14.44-11.42 25.5-27.63 37.15-35.16 56.37-35.16 8.28 0 14.54-1.95 22-6.3 1.78-1.03 13.82-8.82 18.16-11.27 2.83-1.59 5.66-3.03 8.63-4.39 7.92-3.6 13.97-4.45 26.6-4.8 7.53-.2 10.7-.49 14.26-1.58 4.55-1.4 8.06-4 10.93-8.43 2.2-3.41 6.85-7.08 14.66-12.06 1.61-1.03 3.27-2.05 5.65-3.5 9.53-5.85 11.56-7.13 14.81-9.57 5.34-4 9.3-8.37 13.68-14.77a204.2 204.2 0 0 0 5.62-8.75v1.9c-1.97 3.17-3.4 5.38-4.8 7.42-4.42 6.48-8.46 10.92-13.9 15-3.29 2.46-5.32 3.75-14.89 9.61a375.06 375.06 0 0 0-5.63 3.5c-7.7 4.9-12.26 8.52-14.36 11.76-3 4.63-6.7 7.39-11.48 8.85-3.68 1.12-6.9 1.42-14.53 1.63-12.5.34-18.44 1.18-26.2 4.7a111.08 111.08 0 0 0-8.56 4.35c-4.3 2.43-16.34 10.22-18.15 11.27-7.6 4.43-14.03 6.43-22.5 6.43-18.87 0-30.3 7.4-55.63 34.84-4.88 5.28-9.67 8.97-14.7 11.62-2 1.05-4 1.92-6.23 2.75-.86.32-1.7.62-5.37 1.87-5.08 1.76-7.44 3.25-9.28 6.37-2.23 3.78-3.29 9.94-3.29 20.05 0 17.9-1.87 24.07-7.8 29.89-.69.67-3.27 3.06-3.69 3.46a58.04 58.04 0 0 0-4.28 4.49c-3.53 4.14-6.86 9.32-10.28 16.38a95.19 95.19 0 0 0-4.5 10.99c-3.38 9.97-5.18 20.48-5.76 31.9-.44 8.6-.22 16.17.55 28.17.69 10.76.76 12.12.76 15.72 0 6.35-1.02 10.87-4.35 19zm25.08 0h-1c-.04-4.73.06-9.39.28-15.02.26-6.41-.4-11.79-2.53-24.37l-.31-1.86c-2.12-12.55-2.76-19.35-1.97-26.47 1.03-9.25 4.75-16.68 12-22.67 22.04-18.2 29.81-30.18 29.81-44.61 0-2.6-.3-4.81-.98-8.17-.97-4.79-1.1-5.68-.97-7.57.2-2.56 1.27-4.7 3.56-6.72 2.67-2.35 7.05-4.6 13.72-7.01 9.72-3.5 15.52-9.18 24.3-21.57l1.78-2.5c4.48-6.33 7.1-9.63 10.43-12.78 4.31-4.07 8.98-6.77 14.54-8.17 13.3-3.32 20.37-5.47 25.34-7.64a49.5 49.5 0 0 0 5.28-2.7c1.1-.65 1.75-1.04 4.24-2.6 2.7-1.68 5.22-2.08 11.38-2.28 5.44-.18 7.9-.43 10.97-1.41a21.47 21.47 0 0 0 9.54-6.22c4.87-5.3 10.03-7.61 17.79-8.9 1.07-.18 1.88-.3 3.86-.58 6.9-.97 9.94-1.69 13.48-3.62 4.5-2.45 6.79-4.44 23.46-19.68l3.14-2.85c9.65-8.71 16.12-13.83 21.42-16.48 4.25-2.12 7.6-4.69 11.22-8.6v1.45c-3.42 3.57-6.69 6-10.78 8.05-5.18 2.59-11.61 7.67-21.2 16.32l-3.12 2.85c-16.8 15.35-19.05 17.3-23.66 19.82-3.68 2-6.8 2.75-13.82 3.73-1.97.28-2.78.4-3.84.57-7.56 1.26-12.52 3.48-17.21 8.6a22.47 22.47 0 0 1-9.97 6.5c-3.2 1-5.72 1.27-11.25 1.45-5.98.2-8.39.57-10.89 2.13a144 144 0 0 1-4.25 2.61 50.48 50.48 0 0 1-5.39 2.75c-5.04 2.2-12.15 4.37-25.5 7.7-9.74 2.44-15.26 7.65-24.4 20.56l-1.77 2.5c-8.9 12.54-14.82 18.34-24.78 21.93-6.57 2.36-10.85 4.57-13.4 6.82-2.1 1.86-3.05 3.74-3.22 6.04-.13 1.76 0 2.63.95 7.3.7 3.42 1 5.7 1 8.37 0 14.79-7.93 27-30.18 45.39-7.03 5.8-10.64 13-11.64 22-.78 7-.14 13.73 1.96 26.2l.32 1.85c2.15 12.65 2.8 18.07 2.54 24.58-.22 5.57-.32 10.2-.28 14.98zM95.9 600h-2.04c.68-3.82 1.14-8.8 1.61-15.98.2-3.11.27-4.06.39-5.6 1.3-17.54 4.04-27.14 11.5-33.2 4.65-3.77 7.22-8.92 8.67-16 .51-2.52.7-3.87 1.33-9.17.66-5.5 1.16-8.06 2.24-10.36 1.45-3.09 3.82-4.69 7.39-4.69 14.28 0 38.48 9.12 53.6 20.2 8.66 6.35 21.26 13.32 31.74 17.11 13.03 4.71 21.89 4.41 24.75-1.73 1.7-3.64 1.92-4.11 2.65-5.77 2.93-6.67 4.69-12.2 5.25-17.5.23-2.17.24-4.23.02-6.2-.32-2.75-1.42-4.55-4.08-7.35l-1.32-1.37a30.59 30.59 0 0 1-2.41-2.79 30.37 30.37 0 0 1-2.5-4.07l-1.13-2.14c-1.62-3.1-2.68-4.6-4.12-5.56-5.26-3.5-14.8-5.5-28.55-6.83a272.42 272.42 0 0 0-9.04-.71l-2.18-.17c-9.57-.73-15.12-1.56-19.06-3.2C156.57 471.07 136 450.5 136 440c0-5.34 1.74-9.53 5.47-14.13 1.98-2.44 11.12-11.71 12.79-13.54 4.52-4.97 10.16-9.54 17.68-14.66 2.8-1.9 14.78-9.6 17.49-11.49a50.54 50.54 0 0 0 6.34-5.43c1.53-1.5 6.96-7.13 7.12-7.3 7.18-7.3 12.7-11.56 19.74-14.38 3.36-1.34 8.13-2.79 17.45-5.38a9577.18 9577.18 0 0 1 11.78-3.28 602.6 602.6 0 0 0 12.67-3.7c20.4-6.24 34-12.08 40.79-18.44 8.74-8.2 11.78-13.84 15.73-26.02 2.02-6.22 3.09-9.04 5.07-12.72 9.54-17.71 28.71-39.37 43.5-45.45C383.77 238.25 389 232.34 389 226c0-2.89 2.73-8.4 6.83-13.73 4.76-6.2 10.65-11.36 16.75-14.18 12.5-5.77 33.5-10.09 47.42-10.09 5.32 0 9.83-1.5 16.42-4.89 9.2-4.71 10.1-5.11 13.58-5.11 10.42 0 32.06-2.55 45.76-5.97l3.88-.98 3.47-.89c2.6-.66 4.33-1.08 5.93-1.43 3.9-.86 6.76-1.23 9.58-1.17 2.74.06 5.47.52 8.67 1.48 4.56 1.37 13.71-.9 22.87-5.68a68.07 68.07 0 0 0 9.84-6.2v2.4c-11.09 8.14-25.76 13.66-33.29 11.4a29.72 29.72 0 0 0-8.13-1.4c-2.63-.05-5.36.3-9.11 1.12a238 238 0 0 0-9.33 2.3l-3.9.99C522.38 177.43 500.58 180 490 180c-2.99 0-3.91.4-12.67 4.89-6.85 3.51-11.61 5.11-17.33 5.11-13.65 0-34.35 4.26-46.58 9.9-5.78 2.67-11.42 7.62-16 13.58-3.85 5.02-6.42 10.2-6.42 12.52 0 7.27-5.8 13.82-20.62 19.92-14.27 5.88-33.16 27.21-42.5 44.55-1.9 3.55-2.95 6.28-4.93 12.4-4.05 12.47-7.23 18.39-16.27 26.86-7.08 6.64-20.87 12.57-41.57 18.89a604.52 604.52 0 0 1-12.7 3.71 1495.1 1495.1 0 0 1-11.8 3.28c-9.24 2.58-13.97 4.01-17.24 5.32-6.73 2.69-12.05 6.8-19.05 13.92-.15.15-5.6 5.8-7.15 7.32a52.4 52.4 0 0 1-6.6 5.65c-2.74 1.92-14.75 9.63-17.5 11.5-7.4 5.04-12.94 9.52-17.33 14.35-1.72 1.9-10.8 11.11-12.71 13.46-3.47 4.26-5.03 8.03-5.03 12.87 0 9.5 20 29.5 33.38 35.08 3.67 1.53 9.1 2.34 18.45 3.05a586.23 586.23 0 0 0 4.34.32c3.24.23 5.07.37 6.93.55 14.08 1.37 23.82 3.4 29.45 7.17 1.82 1.2 3.02 2.91 4.8 6.29l1.11 2.13a28.55 28.55 0 0 0 2.34 3.81c.62.83 1.3 1.6 2.26 2.61.23.24 1.1 1.16 1.32 1.37 2.93 3.09 4.24 5.23 4.61 8.5.24 2.12.23 4.33-.01 6.64-.59 5.55-2.4 11.25-5.41 18.1-.74 1.67-.96 2.15-2.66 5.8-3.49 7.47-13.33 7.8-27.25 2.77-10.67-3.86-23.43-10.92-32.25-17.38C164.62 515.96 140.82 507 127 507c-5 0-6.4 3.02-7.64 13.29a99.03 99.03 0 0 1-1.36 9.33c-1.53 7.5-4.3 13.04-9.37 17.16-6.87 5.58-9.5 14.78-10.77 31.8-.11 1.52-.18 2.47-.38 5.57-.46 7.01-.91 11.99-1.57 15.85zm8.05 0h-1.02c.29-1.41.58-2.94.9-4.59l1.05-5.62c2.5-13.3 4.2-19.92 6.68-24.05 1.7-2.84 3.68-5.5 8.05-11.03 8.21-10.36 10.88-14.55 10.88-18.71l-.02-1.69c-.02-1.78-.02-2.7.02-3.77.21-5.05 1.47-8.2 4.64-9.4 3.92-1.5 10.39.44 20.12 6.43 9.56 5.88 17.53 10.7 25.91 15.66 1.31.78 14.27 8.41 17.67 10.45a714.21 714.21 0 0 1 6.42 3.9c13.82 8.5 38.94 5.05 46.3-7.83 3.6-6.28 4.54-8.52 7.78-17.32a82.3 82.3 0 0 1 1.18-3.07 42.27 42.27 0 0 1 4.06-7.64c9.33-13.98 14.92-26.1 14.92-36.72 0-3.66.75-6.62 3.36-14.85.52-1.64.83-2.66 1.15-3.73 3.64-12.23 3.04-19.12-4.29-24a23.1 23.1 0 0 0-9.98-3.78c-7.2-.93-14.49 1.17-23.91 5.88-1.55.78-6.64 3.44-7.6 3.93a62.6 62.6 0 0 0-4.14 2.3l-4.4 2.66c-11.62 6.92-20.4 9.18-32.81 6.08-3.32-.84-6.24-1.4-13.1-2.64-13.25-2.39-18.7-3.75-23.33-6.46-6.23-3.67-7.46-9.02-2.88-16.65A93.1 93.1 0 0 1 172 415.42a157 157 0 0 1 8.32-7.66c-.07.05 6.16-5.3 7.82-6.77a85.12 85.12 0 0 0 6.5-6.33c7.7-8.46 12.78-13.36 20.08-18.57 9.94-7.1 21.4-12.36 35.18-15.58 37.03-8.64 51-12.7 58.83-17.93 8.6-5.73 21.3-24.77 36.84-54.81 5.22-10.1 12.27-18.4 21.13-25.71 5.13-4.24 9.56-7.25 17.55-12.23 7.42-4.62 9.62-6.14 11.38-8.16a21.15 21.15 0 0 0 2.95-4.87c.61-1.3 2.87-6.47 3-6.77 1.36-3 2.56-5.4 3.95-7.73 6.53-10.97 16.03-18 31.4-20.8 12.73-2.3 19.85-2.7 29.68-2.3 3.25.13 4.13.16 5.6.14 5.15-.07 9.71-1.04 16.61-3.8 20.74-8.3 38.75-12.04 59.19-12.04 3.05 0 6.03.15 10.48.48l2.09.16c12.45.96 18.08.96 25.34-.63a49.65 49.65 0 0 0 14.09-5.45v1.15a50.52 50.52 0 0 1-13.88 5.28c-7.38 1.61-13.08 1.61-25.63.65l-2.08-.16c-4.43-.33-7.39-.48-10.41-.48-20.3 0-38.2 3.72-58.81 11.96-7.01 2.8-11.7 3.8-16.97 3.88-1.5.02-2.39-.01-5.66-.14-9.76-.4-16.8-.01-29.47 2.3-15.06 2.73-24.32 9.58-30.71 20.31a72.8 72.8 0 0 0-3.9 7.63c-.12.28-2.39 5.47-3.01 6.79a22 22 0 0 1-3.1 5.1c-1.86 2.13-4.07 3.66-11.6 8.35-7.95 4.96-12.35 7.95-17.44 12.15-8.76 7.23-15.73 15.43-20.89 25.4-15.61 30.2-28.36 49.32-37.16 55.19-7.98 5.32-21.97 9.39-59.17 18.07-13.65 3.18-24.98 8.39-34.82 15.42-7.22 5.16-12.27 10.01-19.92 18.43a86.07 86.07 0 0 1-6.57 6.4c-1.67 1.48-7.91 6.83-7.84 6.77-3.27 2.84-5.8 5.16-8.26 7.62a92.1 92.1 0 0 0-14.27 18.13c-4.3 7.16-3.22 11.89 2.53 15.26 4.47 2.63 9.88 3.99 23.24 6.39a185.7 185.7 0 0 1 12.92 2.6c12.11 3.03 20.64.84 32.06-5.96l4.4-2.65c1.66-1 2.96-1.73 4.2-2.35.95-.48 6.04-3.14 7.6-3.92 9.59-4.8 17.04-6.94 24.49-5.98a24.1 24.1 0 0 1 10.4 3.93c7.82 5.21 8.45 12.52 4.7 25.13-.32 1.07-.64 2.1-1.16 3.74-2.57 8.12-3.31 11.04-3.31 14.55 0 10.88-5.66 23.14-15.08 37.28a41.28 41.28 0 0 0-3.97 7.46c-.37.9-.73 1.82-1.18 3.04-3.25 8.85-4.21 11.13-7.84 17.47-7.67 13.42-33.43 16.95-47.7 8.18a578.4 578.4 0 0 0-6.4-3.89c-3.4-2.04-16.36-9.67-17.67-10.45-8.38-4.97-16.36-9.78-25.92-15.66-9.5-5.85-15.7-7.7-19.24-6.36-2.68 1.02-3.8 3.82-4 8.51a61.12 61.12 0 0 0-.02 3.72l.02 1.7c0 4.5-2.69 8.73-11.52 19.87-3.92 4.95-5.87 7.59-7.55 10.39-2.39 3.97-4.08 10.56-6.56 23.72l-1.05 5.62-.86 4.4zm10.5 0h-1c.03-.34.04-.68.04-1 0-12.39 8.48-33.57 19.16-43.37a26.18 26.18 0 0 0 3.67-4.17 35.8 35.8 0 0 0 2.88-4.9c.36-.72 1.75-3.66 2.1-4.36 3.22-6.29 6.84-6.54 16.97.39 1.34.9 6.07 4.16 6.4 4.38 2.62 1.8 4.67 3.2 6.7 4.56 5.03 3.39 9.37 6.2 13.51 8.7 14.33 8.67 25.49 13.27 34.11 13.27 16.86 0 32.71-5.95 39.6-14.8 1.59-2.04 3.2-5.17 5.06-9.63.8-1.92 1.64-4.06 2.67-6.8l2.74-7.33c4.66-12.44 7.76-19.06 11.56-23.27 7.9-8.79 14.87-36 14.87-52.67 0-1.9.17-3.11 1.02-8.27.37-2.2.58-3.6.74-5.07.63-5.51.21-9.46-1.68-12.39-4.6-7.1-19.7-9.23-38.46-4.78a100.57 100.57 0 0 0-18.94 6.3c-5.17 2.37-17.11 9.74-16.5 9.4-6.72 3.64-12.97 4.15-24.8 1.3-29.55-7.14-30.43-8.62-15.26-26.81 17.44-20.93 47.12-46.18 56.38-46.18 9.92 0 53.84-11.98 65.78-17.95 9.46-4.73 24.32-21.18 36.82-37.85.71-.95 13.5-21.6 19.2-29.6 9.35-13.13 18.22-22.55 26.95-27.53 7.29-4.17 13.16-10.28 18.8-18.73 1.93-2.9 10.52-17.65 12.73-20.41 1.54-1.93 3-3.21 4.52-3.89 14.07-6.25 24.22-9.04 39.2-9.04h29c4.05 0 7.36-.4 22.93-2.5l4.3-.57c9.92-1.3 16.57-1.93 21.77-1.93 1.66 0 2.95.01 6.03.04 18.61.19 28.55-.48 44.86-4.03 3.1-.67 6.13-1.78 9.11-3.31v1.12a37.96 37.96 0 0 1-8.9 3.17c-16.4 3.56-26.4 4.24-45.08 4.05-3.08-.03-4.36-.04-6.02-.04-5.15 0-11.76.63-21.64 1.92l-4.3.58c-15.64 2.11-18.94 2.5-23.06 2.5h-29c-14.81 0-24.84 2.75-38.8 8.96-1.34.6-2.69 1.78-4.14 3.6-2.16 2.68-10.72 17.39-12.68 20.33-5.72 8.57-11.7 14.8-19.13 19.04-8.57 4.9-17.36 14.23-26.63 27.24-5.68 7.97-18.47 28.64-19.22 29.63-12.6 16.8-27.52 33.32-37.18 38.15-12.06 6.03-56.14 18.05-66.22 18.05-8.82 0-38.39 25.15-55.62 45.82-14.6 17.52-14.19 18.21 14.74 25.2 11.6 2.8 17.6 2.3 24.09-1.2-.67.35 11.31-7.03 16.56-9.44 5.41-2.48 11.6-4.59 19.11-6.37 19.13-4.53 34.65-2.35 39.54 5.22 2.05 3.17 2.48 7.32 1.84 13.04a96.34 96.34 0 0 1-.75 5.13c-.84 5.08-1.01 6.29-1.01 8.1 0 16.9-7.03 44.33-15.13 53.33-3.68 4.09-6.76 10.65-11.37 22.96-.35.93-2.2 5.94-2.73 7.33-1.04 2.76-1.88 4.9-2.68 6.84-1.9 4.53-3.55 7.73-5.2 9.85-7.1 9.13-23.25 15.19-40.39 15.19-8.86 0-20.15-4.65-34.63-13.42-4.15-2.51-8.5-5.32-13.55-8.72a861.54 861.54 0 0 1-6.71-4.56l-6.4-4.39c-9.68-6.63-12.61-6.42-15.5-.75-.35.68-1.74 3.62-2.1 4.35a36.77 36.77 0 0 1-2.96 5.03c-1.12 1.57-2.37 3-3.81 4.33-10.47 9.6-18.84 30.51-18.84 42.63l-.03 1zm-29.65 0h-1.1c1.17-2.52 1.79-5.2 1.79-8 0-20 4.83-42.04 12.15-49.35 5.17-5.18 7.77-8.38 9.9-12.74 2.64-5.41 3.95-12 3.95-20.91 0-6.82 1.14-11.59 3.37-15.07 1.74-2.7 3.6-4.21 8.91-7.52a31.64 31.64 0 0 0 3.9-2.79c4.61-3.96 6.58-6.2 7.72-9.41 1.43-4.02.93-9.04-1.86-16.02a68.98 68.98 0 0 0-3.99-8.07l-.93-1.7a75.47 75.47 0 0 1-2.64-5c-5.16-10.71-3.77-18.9 7.68-29.78a204 204 0 0 1 26.81-21.55c3.96-2.69 16.8-10.8 19.24-12.5 1.99-1.4 4.33-3.3 7.77-6.3-.02 0 7.23-6.39 9.47-8.3 4.97-4.26 9.09-7.5 13.05-10.15 4.72-3.15 8.97-5.28 12.87-6.32 12.78-3.41 15.6-4.18 21.77-5.97 12.55-3.64 21.96-6.9 28.14-10a45.47 45.47 0 0 1 7.47-2.79c8.66-2.66 12.02-4.1 16.97-8.1 6.78-5.46 13.07-14.25 19.33-27.87 15.97-34.77 19.08-39.39 32.15-49.19 3.14-2.36 6.37-4.1 11.43-6.4l2.33-1.04c11.93-5.35 16.87-8.93 21.1-17.38 1.88-3.77 2.48-6.29 3.37-12.27.78-5.19 1.48-7.56 3.53-10.25 2.57-3.4 7.03-6.27 14.36-9.01 3.37-1.26 7.36-2.5 12.05-3.73 16.33-4.3 25.28-5.36 39.6-5.81 6.9-.22 9.5-.56 12.66-2 1.19-.54 2.36-1.23 3.58-2.11 3.7-2.7 8.14-4.54 13.24-5.67 5.71-1.27 10.69-1.54 18.7-1.45l2.35.02c2.82 0 6.8-1 19.7-4.69 10.83-3.08 15.95-4.31 19.3-4.31.82 0 1.9.13 3.55.41l5.01.9c9.82 1.68 17.44 1.89 25.15-.21 7.98-2.18 14.8-6.77 20.29-14.24V147c-5.47 7.04-12.21 11.42-20.03 13.55-7.88 2.15-15.63 1.94-25.58.23l-5-.9c-1.6-.26-2.64-.39-3.39-.39-3.2 0-8.32 1.22-19.74 4.48-12.35 3.53-16.3 4.52-19.26 4.52l-2.36-.02c-7.94-.1-12.85.17-18.47 1.42-4.97 1.11-9.3 2.9-12.88 5.5a21.4 21.4 0 0 1-3.75 2.22c-3.32 1.5-6 1.87-13.04 2.09-14.25.44-23.13 1.5-39.37 5.77a125.56 125.56 0 0 0-11.95 3.7c-7.17 2.7-11.49 5.46-13.93 8.68-1.9 2.52-2.58 4.76-3.33 9.8-.9 6.08-1.53 8.68-3.47 12.56a30.6 30.6 0 0 1-9.66 11.45c-3.12 2.26-5.95 3.73-11.93 6.4l-2.31 1.04c-5.01 2.27-8.18 3.99-11.25 6.29-12.9 9.68-15.93 14.17-31.85 48.8-6.31 13.76-12.7 22.68-19.6 28.25-5.08 4.1-8.53 5.57-17.3 8.27a44.64 44.64 0 0 0-7.33 2.73c-6.24 3.12-15.7 6.4-28.3 10.06a867.4 867.4 0 0 1-21.8 5.97c-3.77 1.01-7.93 3.1-12.56 6.19a137.35 137.35 0 0 0-12.95 10.07c-2.24 1.92-9.48 8.3-9.48 8.3a98.2 98.2 0 0 1-7.84 6.37c-2.46 1.72-15.32 9.83-19.26 12.5a203 203 0 0 0-26.69 21.45c-11.13 10.58-12.43 18.3-7.47 28.63a74.52 74.52 0 0 0 2.62 4.95l.94 1.7a69.84 69.84 0 0 1 4.03 8.17c2.88 7.2 3.4 12.46 1.89 16.73-1.22 3.43-3.28 5.77-8.02 9.84-1.14.97-2.32 1.8-5.3 3.67-3.92 2.45-5.69 3.89-7.31 6.42-2.13 3.3-3.22 7.89-3.22 14.53 0 9.05-1.34 15.79-4.05 21.34-2.19 4.49-4.85 7.77-10.1 13.01-7.07 7.07-11.85 28.9-11.85 48.65 0 2.8-.58 5.48-1.7 8zm282.54 0h-1.01l-1.1-5.8c-3.08-16.26-4.05-26.2-2.74-37.26.7-5.8.77-9.68.55-15.3-.18-4.45-.17-5.68.19-7.63.78-4.3 3.44-8.53 10.39-16.34 9.07-10.2 12.26-15.41 19.8-30.15 1.35-2.64 2.33-4.47 3.38-6.3.9-1.58 1.82-3.06 2.77-4.5 3.14-4.7 7.03-8.42 16.84-16.81 11.22-9.6 15.5-13.86 18.13-19.13.7-1.4 1.3-2.8 1.93-4.4a206 206 0 0 0 1.49-4.05c3.63-9.94 8.01-13.93 22.9-17.81 4.99-1.3 20.55-5.13 21.38-5.34 16.19-4.1 25.33-7.36 33.48-12.6 5.86-3.77 5.84-3.76 27.66-16.53l2.6-1.52c10.23-6 17.1-10.2 22.73-13.95a149.3 149.3 0 0 0 8.8-6.3 723.7 723.7 0 0 0 6.37-5.08A87.74 87.74 0 0 1 600 342.95v1.12a85.76 85.76 0 0 0-15.49 9.9c.18-.14-4.76 3.84-6.38 5.1a150.3 150.3 0 0 1-8.85 6.35c-5.65 3.76-12.53 7.96-22.78 13.97l-2.6 1.53c-21.8 12.75-21.78 12.74-27.63 16.5-8.27 5.32-17.49 8.61-33.78 12.73-.83.21-16.39 4.04-21.36 5.33-8.03 2.1-13.15 4.5-16.45 7.5-2.66 2.42-4 4.86-5.77 9.7l-1.5 4.07a51.12 51.12 0 0 1-1.96 4.47c-2.72 5.45-7.04 9.75-18.38 19.45-9.73 8.32-13.6 12.02-16.65 16.6a77.18 77.18 0 0 0-2.74 4.45c-1.05 1.81-2.01 3.63-3.35 6.25-7.58 14.81-10.82 20.08-19.96 30.36-6.83 7.7-9.4 11.78-10.15 15.86-.34 1.85-.34 3.04-.17 7.4.22 5.68.14 9.6-.55 15.47-1.3 10.92-.34 20.79 2.73 36.95l1.12 5.99zm-76.59 0h-2.1l1.39-4.3c1.04-3.3 1.93-6.78 2.68-10.4 2.65-12.73 3.27-23.63 3.27-41.3 0-5.71-1.86-9.75-4.13-9.75-2.94 0-6.96 5.61-10.93 17.08C271.14 579.68 258.3 593 238 593c-22.42 0-29.26-1.35-48.42-10.09a87.69 87.69 0 0 1-9.42-5.04c-2.95-1.8-12.78-8.57-14.84-9.72-4.2-2.36-7-2.71-9.72-.99-.63.4-1.26.91-1.9 1.55a57.69 57.69 0 0 1-4.31 3.86 147.88 147.88 0 0 1-3.06 2.44l-1 .8C137.01 582.43 134 587.18 134 597c0 1.02-.02 2.01-.07 3h-2c.05-.99.07-1.98.07-3 0-10.52 3.33-15.78 12.09-22.76a265.61 265.61 0 0 1 2-1.6c.83-.64 1.43-1.13 2.03-1.61a55.76 55.76 0 0 0 4.17-3.74c.74-.73 1.48-1.34 2.24-1.82 3.47-2.2 7-1.75 11.77.93 2.15 1.21 12.03 8 14.9 9.76a85.7 85.7 0 0 0 9.22 4.93C209.29 589.7 215.85 591 238 591c19.25 0 31.49-12.7 41.06-40.33 4.24-12.25 8.66-18.42 12.81-18.42 3.8 0 6.13 5.06 6.13 11.75 0 17.8-.63 28.8-3.3 41.7-.77 3.7-1.68 7.23-2.75 10.6-.4 1.3-.8 2.53-1.19 3.7zm-149.25 0l.5-.94a160.1 160.1 0 0 0 6.53-13.26c2.73-6.29 5.78-9.64 9.24-10.52 3.74-.95 7.15.74 12.56 5.13 5.43 4.4 6.07 4.86 7.73 5.1 1.6.22 4.28 1.14 8.86 2.95 1.3.5 10.78 4.35 13.85 5.55 3.07 1.2 5.85 2.25 8.49 3.18 3.1 1.1 5.98 2.04 8.65 2.81h-3.45c-1.76-.56-3.6-1.18-5.54-1.87a281.2 281.2 0 0 1-8.51-3.19c-3.08-1.2-12.57-5.04-13.86-5.55-4.5-1.78-7.15-2.68-8.63-2.9-1.94-.27-2.53-.7-8.22-5.3-5.17-4.2-8.36-5.78-11.69-4.94-3.1.78-5.94 3.92-8.56 9.95a161 161 0 0 1-6.82 13.8h-1.13zm112.89 0a30.34 30.34 0 0 0 11.27-6.27c1.55-1.36 3.32-3.46 5.34-6.29 1.05-1.46 2.15-3.1 3.41-5.04a349.73 349.73 0 0 0 2.5-3.9l.47-.75.93-1.47a89.17 89.17 0 0 1 3.25-4.86c1.05-1.43 1.82-2.23 2.44-2.46 1.02-.37 1.49.48 1.49 2.04l.01 2.11c.05 6.91-.08 11.32-.7 16.33a48.4 48.4 0 0 1-2.38 10.56h-1.07a46.47 46.47 0 0 0 2.45-10.68c.62-4.96.75-9.33.7-16.2l-.01-2.12c0-.97-.08-1.12-.15-1.1-.36.14-1.05.85-1.97 2.1a88.44 88.44 0 0 0-3.22 4.82l-.92 1.46-.48.75a1268.1 1268.1 0 0 1-2.5 3.92c-1.26 1.95-2.38 3.6-3.44 5.08-2.06 2.88-3.87 5.04-5.5 6.45a30.87 30.87 0 0 1-8.94 5.52h-2.98zm-183.72 0H69.3c3.37-3.43 5.19-8.33 5.19-15 0-18.6-.04-17.35 1.02-20.77.6-1.93 1.5-3.74 3.27-6.63.42-.7 4.92-7.8 6.78-10.86 3.04-4.97 11.04-16.5 12.21-18.56 3.48-6.08 4.72-12.06 4.72-24.18 0-7.85 2.5-14.2 8.1-23.44l2.84-4.63a72.67 72.67 0 0 0 2.49-4.4c1.62-3.15 2.48-5.78 2.62-8.28.2-3.78-1.3-7.29-4.9-10.9-5.13-5.12-8.6-5.43-11.2-1.85-2.12 2.92-3.48 7.74-5.06 16.47-.2 1.03-.82 4.6-.82 4.57-.83 4.67-1.4 7.33-2.1 9.6-1.35 4.42-3.7 7.61-8.36 12.26l-3.26 3.2c-6.38 6.39-9.68 11.51-11.36 19.5l-1.16 5.52c-.87 4.1-1.56 7.04-2.33 9.94-3.67 13.74-9.65 25.97-22.59 44.72-7.68 11.14-11.05 18.87-10.92 23.72h-1c-.12-5.16 3.35-13.05 11.1-24.28 12.87-18.67 18.8-30.8 22.44-44.42.77-2.88 1.45-5.8 2.32-9.89l1.16-5.51c1.73-8.22 5.13-13.5 11.64-20 .63-.64 2.84-2.8 3.25-3.21 4.57-4.54 6.82-7.62 8.12-11.84a81.58 81.58 0 0 0 2.07-9.48l.81-4.57c1.62-8.9 3-13.8 5.24-16.89 3-4.15 7.2-3.78 12.71 1.74 3.8 3.8 5.42 7.58 5.2 11.66-.15 2.66-1.05 5.41-2.73 8.68a73.6 73.6 0 0 1-2.52 4.46l-2.84 4.63c-5.52 9.1-7.96 15.3-7.96 22.92 0 12.28-1.28 18.43-4.85 24.68-1.2 2.1-9.21 13.65-12.22 18.58-1.87 3.06-6.37 10.18-6.78 10.86-1.73 2.82-2.6 4.57-3.17 6.4-1.02 3.28-.98 2.1-.98 20.48 0 6.52-1.7 11.44-4.82 15zM310.09 0h1.06c-.37.9-.77 1.83-1.2 2.82-3.9 9.06-5.45 15.15-5.45 25.18 0 7.64-2.1 11.6-6.64 13.05-3.46 1.1-5.72.98-17.57-.43-11.55-1.36-19.17-1.58-28.16-.14-6.24 2.49-25.91 7.02-32.13 7.02-11.15 0-36.76-2.88-54.12-7.01a22.08 22.08 0 0 0-16.95 2.48c-4.05 2.33-7.09 5.03-13.9 11.97-6.28 6.39-9.53 9.23-13.8 11.5-7.09 3.79-11.22 7.65-13.4 12.27-1.82 3.85-2.33 7.84-2.33 15.29 0 4.4-2.65 6.69-9.45 9.74.1-.05-2.97 1.31-3.84 1.71-8.78 4.06-12.71 8.29-12.71 16.55 0 12.52-4.86 19.22-17.34 27.96l-4.56 3.14c-1.9 1.3-3.3 2.3-4.67 3.3-.92.68-1.79 1.34-2.62 2-7.16 5.62-11 14.54-15.56 33.28-.63 2.57-3.3 14-4.07 17.14a350.44 350.44 0 0 1-5.2 19.33c-1.37 4.5-4.5 15.07-4.96 16.53-1.05 3.4-1.64 4.94-2.46 6.32-.82 1.4-6.85 9.08-12.64 18.27L0 277.98v-1.9l4.58-7.35a270.8 270.8 0 0 1 12.61-18.23c-.3.5 1.35-2.8 2.38-6.12.45-1.44 3.58-12.01 4.95-16.53 1.83-6.03 3.44-12.09 5.19-19.27.76-3.13 3.44-14.56 4.06-17.14 4.62-18.95 8.52-28.02 15.92-33.83.84-.67 1.72-1.33 2.65-2.01 1.38-1.02 2.8-2.01 4.7-3.32l4.54-3.14C73.83 140.57 78.5 134.13 78.5 122c0-8.74 4.2-13.26 13.29-17.45.88-.41 3.96-1.77 3.85-1.73 6.46-2.9 8.86-4.97 8.86-8.82 0-7.6.53-11.7 2.42-15.71 2.29-4.84 6.57-8.85 13.84-12.73 4.15-2.21 7.35-5 14.15-11.93 6.28-6.4 9.36-9.13 13.52-11.53a23.07 23.07 0 0 1 17.69-2.59c17.27 4.12 42.8 6.99 53.88 6.99 6.1 0 25.73-4.53 31.92-7 9.12-1.46 16.83-1.25 28.49.13 11.63 1.38 13.9 1.5 17.15.47 4.06-1.3 5.94-4.85 5.94-12.1 0-10.1 1.56-16.3 6.6-28zm25.12 0h1c.05 5.62.26 11.48.65 19.4.47 9.7.64 14.57.64 21.6 0 9.81-4.68 17.46-13.1 23.16-6.53 4.43-14.94 7.46-24.33 9.33-3.74.54-9.42.56-22.68.23-6.74-.17-9.35-.22-12.39-.22-2.77 0-4.97.43-7.63 1.36-.88.3-4.55 1.74-5.58 2.11-6.55 2.35-13.59 3.53-24.79 3.53-8.1 0-13.58-1.38-22.46-4.9l-3.18-1.25c-12.55-4.87-21.27-5.15-37.18 1.12-11.15 4.39-18.13 9.2-22.28 14.81-3.15 4.26-4.33 7.8-5.94 15.8-1.22 6.09-1.93 8.74-3.5 12.13-1.65 3.53-3.97 5.81-7.07 7.22-2.33 1.07-4.35 1.5-9.32 2.19-9.04 1.27-12.77 3.09-15.61 9.58-3.71 8.48-7.72 13.87-14.22 19.76-2.4 2.18-13.14 11.02-15.91 13.42-8.2 7.1-13.85 17.37-18.7 31.97a258.81 258.81 0 0 0-3.27 10.7c-.01.05-2.26 7.97-2.88 10.1-8.49 28.85-17.88 52.95-26.13 61.2-2.8 2.8-5.06 5.64-10.4 12.96-3.4 4.68-6.23 8.25-8.95 11.1v-1.55c2.74-2.98 5.73-6.82 9.48-11.97 4.03-5.52 6.32-8.4 9.17-11.24 8.07-8.08 17.44-32.14 25.87-60.8.62-2.1 2.86-10.03 2.88-10.08 1.21-4.24 2.21-7.53 3.28-10.74 4.9-14.75 10.63-25.16 19-32.4 2.78-2.42 13.5-11.25 15.89-13.4 6.4-5.8 10.32-11.09 13.97-19.43 1.68-3.83 4.05-6.31 7.2-7.86 2.4-1.17 4.64-1.67 9.53-2.36 4.54-.63 6.5-1.05 8.7-2.06 2.89-1.31 5.03-3.42 6.58-6.73 1.53-3.3 2.23-5.9 3.43-11.9 1.64-8.14 2.85-11.79 6.11-16.2 4.28-5.79 11.41-10.7 22.73-15.16 16.15-6.36 25.13-6.07 37.9-1.11l3.19 1.26c8.77 3.47 14.13 4.82 22.09 4.82 11.09 0 18.02-1.16 24.46-3.47 1-.36 4.68-1.8 5.58-2.11A22.5 22.5 0 0 1 265 72.5c3.05 0 5.67.05 14.07.26 11.53.29 17.2.27 20.83-.25 9.25-1.85 17.54-4.83 23.94-9.17C332 57.8 336.5 50.46 336.5 41c0-7-.17-11.86-.7-22.7-.35-7.26-.55-12.83-.59-18.3zM93.87 0h2.04c-.7 4-1.61 6.82-3.03 9.47-2.33 4.38-2.85 5.75-5.26 13.03a40.46 40.46 0 0 1-1.94 5.03c-2.24 4.66-5.92 8.8-13.07 14.26-8.01 6.13-14.27 16.55-20.03 31.55-2.4 6.23-8.75 25.63-9.64 28.01-2.69 7.16-6.56 12.7-15.63 23.68l-2.68 3.24c-6.02 7.34-9.35 12.07-11.72 17.15-2.3 4.94-7.12 9.9-12.91 14.15v-2.4c5.14-3.94 9.1-8.3 11.1-12.6 2.46-5.27 5.87-10.1 11.98-17.56l2.68-3.26c8.94-10.8 12.72-16.22 15.3-23.1.88-2.33 7.24-21.74 9.65-28.03 5.89-15.31 12.3-26 20.68-32.41 6.92-5.3 10.4-9.2 12.48-13.55.65-1.35 1.16-2.7 1.85-4.79 2.45-7.4 3-8.83 5.4-13.34A27.68 27.68 0 0 0 93.87 0zm9.07 0h1.02c-1.66 8.3-2.91 12.67-4.54 15.26a59.14 59.14 0 0 0-4.1 8.21c-1.27 3-2.44 6.2-3.5 9.4-.38 1.12-.7 2.16-2.41 5.39a251.48 251.48 0 0 0-12.81 13.3c-3.48 3.96-5.95 7.27-7.15 9.66-.95 1.9-2.06 5.99-3.61 12.97-.64 2.9-3.65 17.15-4.51 21.07-3.63 16.45-6.63 26.69-9.9 32-7.66 12.45-10.64 15.71-37.08 41.1A69.78 69.78 0 0 1 0 179.21v-1.15a69.39 69.39 0 0 0 13.65-10.42c26.4-25.33 29.32-28.55 36.92-40.9 3.2-5.18 6.18-15.37 9.78-31.7.86-3.91 3.87-18.16 4.51-21.06 1.57-7.09 2.7-11.2 3.7-13.2 1.24-2.5 3.76-5.86 7.29-9.89.9-1.03 1.86-2.1 2.86-3.18 2.4-2.6 4.96-5.22 7.53-7.76.9-.88 1.73-1.7 3.37-3.4a129.02 129.02 0 0 1 4.78-13.46 60.07 60.07 0 0 1 4.19-8.35c1.52-2.44 2.74-6.71 4.36-14.74zM83.71 0h1.1c-2.09 4.74-6.03 8.92-11.42 12.3-7.2 4.52-16.5 7.2-24.39 7.2-8.9 0-11.8 7-11.74 21.52 0 1.7.04 3.17.12 5.99.1 3.3.12 4.45.12 5.99 0 5.73-.76 11.3-2.01 16.5a66.67 66.67 0 0 1-2.15 6.97 2597.76 2597.76 0 0 1-7 15.86A4270.8 4270.8 0 0 1 6.44 136.2 54.64 54.64 0 0 1 0 147v-1.65a54.87 54.87 0 0 0 5.55-9.57A4269.82 4269.82 0 0 0 30.7 79.97c.53-1.2.99-2.23 2.44-5.9A69.23 69.23 0 0 0 36.5 53c0-1.52-.03-2.66-.12-5.95-.08-2.83-.12-4.31-.12-6.01-.03-6.79.53-11.62 2.07-15.34 1.94-4.68 5.39-7.19 10.67-7.19 7.7 0 16.81-2.63 23.86-7.05C77.93 8.27 81.66 4.38 83.7 0zm282.63 0h1.01c1.86 10.02 2.18 12.67 2.32 18.3a123.43 123.43 0 0 1 .37 27.83c-.96 8.78-3.1 16.01-6.63 21.15-11.34 16.5-39.8 29.22-66.41 29.22-5.09 0-10.47.28-16.31.83a413.8 413.8 0 0 0-24.37 3.16c-21.56 3.26-27.66 4.01-36.32 4.01-6.92 0-12.2-1.05-21.69-3.9l-2.78-.83c-1.39-.41-2.54-.74-3.65-1.02-8-2.05-14.22-2.04-21.7.72a16.32 16.32 0 0 0-9.17 8.18c-1.6 3.05-2.5 6.06-4.02 12.83-1.5 6.64-2.34 9.52-3.99 12.64a16.16 16.16 0 0 1-9.85 8.36 104.8 104.8 0 0 0-9.5 3.42c-6.55 2.8-10.1 5.57-13.8 10.47-1.33 1.75-1.03 1.3-5.43 7.9-1.98 2.97-4.66 5.8-8.48 9.14-2.01 1.76-10.71 8.83-12.88 10.7-7.37 6.35-12.58 12.14-16.63 19.14-4.22 7.3-7.8 18.3-11.28 33.26-.87 3.73-1.72 7.64-2.64 12.14l-1.18 5.8-1.09 5.45c-1.8 8.96-2.77 13.28-3.77 16.26-6.8 20.44-17.26 42.16-27.13 51.2-5.11 4.7-8.1 7.07-11.1 8.86-.9.54-1.84 1.04-2.92 1.57-.44.22-9.6 4.4-14.1 6.66l-1.22.62v-1.13l.78-.39c4.52-2.26 13.67-6.44 14.1-6.65a41.19 41.19 0 0 0 2.84-1.54c2.94-1.75 5.88-4.09 10.94-8.73 9.71-8.9 20.1-30.51 26.87-50.79.97-2.92 1.94-7.22 3.73-16.13l1.1-5.46a490.5 490.5 0 0 1 3.82-17.96c3.5-15.06 7.1-26.14 11.39-33.54 4.11-7.11 9.4-12.98 16.83-19.4 2.19-1.88 10.88-8.95 12.88-10.7 3.77-3.28 6.39-6.05 8.3-8.93 4.43-6.64 4.12-6.18 5.47-7.96 3.8-5.03 7.5-7.91 14.21-10.78 2.61-1.12 5.74-2.24 9.59-3.46a15.17 15.17 0 0 0 9.27-7.86c1.59-3.02 2.42-5.85 4.03-12.99 1.41-6.27 2.32-9.33 3.98-12.48a17.31 17.31 0 0 1 9.7-8.66c7.7-2.83 14.1-2.84 22.3-.75 1.12.29 2.28.61 3.68 1.03l3.73 1.11c8.47 2.54 13.66 3.58 20.46 3.58 8.59 0 14.67-.75 36.18-4a414.64 414.64 0 0 1 24.41-3.17c5.88-.54 11.29-.83 16.41-.83 26.3 0 54.45-12.58 65.59-28.78 3.42-4.98 5.5-12.06 6.46-20.7.84-7.74.73-16.02.02-23.9a136.2 136.2 0 0 0-.57-5.12c0-4.47-.3-6.94-2.16-17zM18.88 0h1.03C18 7.57 17.15 10.18 14.46 16.2c-1.95 4.37-2.67 9.19-2.42 14.89.2 4.33.71 7.7 2.28 16.13 1.09 5.88 1.57 8.77 1.94 12.2.96 8.9.24 16.08-2.8 22.79A463.4 463.4 0 0 1 0 109.43v-2.12a465 465 0 0 0 12.54-25.52c2.97-6.52 3.67-13.53 2.72-22.27-.36-3.4-.84-6.26-1.93-12.12-1.57-8.47-2.1-11.88-2.29-16.27-.26-5.84.48-10.81 2.5-15.33 2.64-5.9 3.48-8.47 5.34-15.8zm280.47 0a70.78 70.78 0 0 1-4.91 11.24c-2.56 4.7-4.01 8.45-4.86 11.98l-.4 1.8-.28 1.45a5.28 5.28 0 0 1-.74 2.07c-.74 1.03-1.93 1.28-5.13 1.25.92 0-9.85-.29-15.03-.29-10.2 0-18.45.82-29.46 2.56-16.87 2.66-17.73 2.77-23.66 2.52a42.57 42.57 0 0 1-8-1.09c-17.7-4.16-46.18-5.86-54.72-3.01-2.72.9-5.88 2.8-9.52 5.59a112.37 112.37 0 0 0-6.54 5.48c-1.4 1.25-9.17 8.5-10.78 9.84-1.45 1.2-8.18 7.42-8.85 8.02a114.65 114.65 0 0 1-4.55 3.9c-4.99 4.03-8.9 6.2-11.92 6.2-3.52.05-4.32 0-5.14-.4-1.13-.56-1.5-1.72-1.13-3.57.74-3.63 4.47-10.84 12.84-24.8 5.69-9.48 9.42-18 11.78-26.2 1.45-5.04 1.94-7.4 2.97-14.54h1.01c-1.05 7.3-1.54 9.7-3.01 14.82-2.39 8.28-6.16 16.89-11.9 26.44-8.3 13.84-12 21.01-12.7 24.48-.3 1.45-.08 2.14.59 2.47.6.3 1.35.35 3.48.3 3.92 0 7.69-2.1 12.5-5.98 1.4-1.13 2.87-2.39 4.51-3.86.66-.59 7.41-6.83 8.88-8.05 1.59-1.33 9.34-8.55 10.75-9.82 2.4-2.15 4.55-3.96 6.6-5.53 3.72-2.85 6.97-4.8 9.81-5.74 8.76-2.92 37.41-1.22 55.27 2.99 2.57.6 5.14.95 7.81 1.06 5.84.25 6.7.14 23.47-2.51 11.05-1.75 19.36-2.57 29.6-2.57 5.2 0 15.99.3 15.05.29 2.87.03 3.84-.17 4.3-.83.23-.32.4-.8.58-1.7l.28-1.43.4-1.85c.88-3.6 2.36-7.44 4.96-12.22 1.87-3.43 3.44-7 4.73-10.76h1.06zm-8.59 0c-5.91 17.94-9.55 22-19.76 22-4.5 0-10.22.32-28.69 1.5l-1.53.1c-15.6.99-23.47 1.4-28.78 1.4-5.35 0-13.24-.96-28.86-3.28l-1.54-.23C163.18 18.75 157.47 18 153 18c-4.45 0-7.3 1.01-10.96 3.34-.1.06-1.8 1.17-2.3 1.47-2.43 1.5-4.32 2.19-6.74 2.19-2.8 0-4.11-1.46-4.11-4.22 0-1.04.16-2.29.5-4.1.16-.82.9-4.4 1.07-5.32.8-4.11 1.3-7.68 1.47-11.36h2c-.17 3.82-.68 7.5-1.5 11.75-.19.94-.92 4.5-1.07 5.31a21.04 21.04 0 0 0-.47 3.72c0 1.7.46 2.22 2.11 2.22 1.99 0 3.55-.57 5.7-1.9.47-.28 2.15-1.37 2.26-1.44C144.92 17.14 148.12 16 153 16c4.62 0 10.3.74 28.9 3.51l1.53.23C198.93 22.04 206.8 23 212 23c5.25 0 13.11-.41 28.65-1.4l1.54-.1C260.73 20.32 266.43 20 271 20c8.95 0 12.15-3.4 17.66-20h2.1zM141.51 0h1.13c-2.06 3.86-2.63 5.1-2.77 6.19-.15 1.12.42 1.64 2.32 1.96 1.8.3 3.85.35 10.81.35 6.02 0 13 .56 21.35 1.62 3.95.5 8.03 1.1 13.13 1.89 24 3.7 22.5 3.49 26.83 3.49 24.02 0 51.83-2.24 60.45-6.94 2.88-1.57 5.05-4.49 6.6-8.56h1.07c-1.64 4.47-3.98 7.69-7.2 9.44-8.83 4.82-36.67 7.06-60.92 7.06-4.41 0-2.84.22-26.98-3.5-5.1-.8-9.17-1.38-13.1-1.88-8.31-1.06-15.26-1.62-21.23-1.62-7.04 0-9.1-.05-10.97-.37-2.38-.4-3.38-1.32-3.15-3.07.16-1.22.69-2.41 2.63-6.06zm76.4 0c5.69 1.64 10.37 2.5 14.09 2.5 9.59 0 16.7-.71 22.4-2.5h2.98C251.12 2.53 243.2 3.5 232 3.5c-4.5 0-10.32-1.21-17.53-3.5h3.45zM70.69 0c-2.87 3.27-6.95 5.39-12.02 6.53-3.98.89-7.5 1.08-12.92 1A97.24 97.24 0 0 0 44 7.5c-5.37 0-8.86-1.24-10.1-4.97A8.6 8.6 0 0 1 33.5 0h.99c.02.82.14 1.56.36 2.22C35.91 5.39 39.02 6.5 44 6.5l1.76.02c5.35.09 8.8-.1 12.69-.97C62.95 4.54 66.63 2.74 69.3 0h1.37zM0 207.87c7.31-.16 11.5 3.33 11.5 11.13 0 11.41-5.05 28.35-11.5 41.5v-2.3c5.93-12.72 10.5-28.47 10.5-39.2 0-7.18-3.7-10.3-10.5-10.13v-1zm0 7.05c1.23.14 2.18.58 2.87 1.31 1.4 1.48 1.6 3.72 1.16 7.58l-.16 1.3A28.93 28.93 0 0 0 3.5 229c0 3.2-1.48 9.52-3.5 15.9v-3.45c1.49-5.13 2.5-9.87 2.5-12.45 0-.98.08-1.75.37-4.02l.16-1.29c.42-3.56.24-5.59-.88-6.77-.5-.53-1.21-.87-2.15-1v-1zM0 410.9v-1.47a21.67 21.67 0 0 0 2.97-4.7c1.32-2.7 2.68-6.28 4.56-11.89 7.85-23.55 7.83-26.6.25-30.4-2.25-1.12-4.8-1.43-7.78-.91v-1.02a13.1 13.1 0 0 1 8.22 1.04c8.24 4.12 8.26 7.6.25 31.6-1.88 5.66-3.25 9.27-4.6 12.02A20.82 20.82 0 0 1 0 410.9zM33.64 452c1.68 0 3.04-.23 8.34-1.31l2.38-.47c8.26-1.57 12.72-1.3 14.53 2.33 1.38 2.75-.47 5.86-4.75 9.68a75.6 75.6 0 0 1-5.08 4.07c-.94.7-4.89 3.59-5.79 4.27-1.86 1.4-2.97 2.37-3.47 3.03a19.08 19.08 0 0 0-2.89 5.5c.07-.2-4.02 13.65-6.96 22.22-2.7 7.85-5.56 10.72-8.82 8.59-2.11-1.4-3.66-4.24-6.6-11.03-1.98-4.62-2.5-5.76-3.4-7.4-4.55-8.18-3.9-23.9-.05-32.87a9.6 9.6 0 0 1 6.98-5.96c2.59-.66 4.86-.75 11.78-.67l3.8.02zm0 2c-1.13 0-2.09 0-3.82-.02-12.07-.13-14.83.57-16.9 5.41-3.63 8.47-4.26 23.55-.05 31.12.96 1.73 1.48 2.88 3.5 7.58 2.72 6.3 4.24 9.08 5.86 10.14 1.64 1.08 3.5-.8 5.82-7.55a682.9 682.9 0 0 0 6.97-22.24 21.03 21.03 0 0 1 3.18-6.04c.65-.87 1.85-1.9 3.86-3.43.92-.7 4.87-3.57 5.8-4.27 2.02-1.5 3.6-2.77 4.95-3.97 3.63-3.23 5.09-5.7 4.3-7.28-1.21-2.42-5.07-2.65-12.38-1.27l-2.35.47c-5.49 1.11-6.86 1.35-8.74 1.35zm345.63 146c-3.45-12.26-3.77-14.13-3.77-19 0-3.33-.13-6.27-.43-11.34-.63-10.33-.65-13.5.26-17.07 1.21-4.74 4.21-7.1 9.67-7.1h26c4.08 0 5.19 1.85 5.93 7.11.1.79.13.97.19 1.32.84 5.35 2.8 7.58 8.88 7.58 3.64 0 5.54.4 6.43 1.37.76.83.76 1.44.36 3.93-.85 5.26.5 8.85 7.5 13.8 6.32 4.45 11.63 5.36 16.55 3.37 3.8-1.54 6.73-4.16 11.92-10l1.1-1.23 1.09-1.23a75.6 75.6 0 0 1 2.7-2.86 35.81 35.81 0 0 1 9.57-6.73c1.52-.76 1.72-.86 5.66-2.63 6.1-2.73 9.01-4.5 11.74-7.62 2.63-3 4.67-4.85 6.7-6.04 3.18-1.85 5.46-2.13 13.68-2.13 5.98 0 10.56-4.32 18-14.99l2.82-4.03c1.06-1.5 1.94-2.7 2.79-3.79 7.87-10.12 19.38-10.4 30.74.96 5.54 5.53 10.17 19.43 13.64 38.51 2.5 13.75 4.18 29.46 4.47 39.84h-1c-.3-10.32-1.96-25.97-4.45-39.66-3.43-18.87-8.02-32.65-13.36-37.99-10.95-10.95-21.76-10.68-29.26-1.04-.83 1.07-1.7 2.26-2.75 3.75l-2.81 4.02c-7.65 10.95-12.38 15.42-18.83 15.42-8.04 0-10.21.26-13.17 2-1.92 1.12-3.9 2.9-6.45 5.83-2.86 3.26-5.87 5.09-12.09 7.88a103.35 103.35 0 0 0-5.62 2.6 34.84 34.84 0 0 0-9.32 6.54 74.67 74.67 0 0 0-3.75 4.05l-1.1 1.24c-5.28 5.95-8.29 8.64-12.28 10.25-5.26 2.13-10.92 1.17-17.5-3.48-7.33-5.17-8.82-9.15-7.92-14.77.34-2.12.34-2.6-.1-3.1-.64-.69-2.34-1.04-5.7-1.04-6.63 0-8.96-2.63-9.87-8.42l-.2-1.34c-.67-4.82-1.53-6.24-4.93-6.24h-26c-5 0-7.6 2.04-8.7 6.34-.88 3.43-.85 6.57-.23 16.76a177 177 0 0 1 .43 11.4c0 4.78.32 6.63 3.81 19h-1.04zm13.68 0c-1.31-6.58-1.61-10.71-1.36-14.84.04-.7.1-1.44.18-2.38l.23-2.56c.34-3.81.5-6.97.5-11.22 0-4.94 1.46-7.76 4.21-8.42 2.38-.58 5.56.54 9.2 3 6.64 4.52 13.99 13.07 16.55 19.23 4.77 11.44 14.12 15.69 33.54 15.69 8.6 0 14.32-2.35 20.67-7.88 1.45-1.26 15.06-15 21-20 7.21-6.07 11.77-7.59 20.62-8.32 5.52-.45 7.98-.9 11.44-2.36 4.58-1.95 9.36-5.48 14.9-11.29 7.43-7.76 13.25-8.92 17.47-4.3 3.32 3.63 5.46 10.58 6.82 20.24.73 5.17.94 7.74 1.58 17.38.25 3.75.17 5.32-.92 18.03h-1c1.09-12.7 1.17-14.28.92-17.97-.64-9.6-.85-12.16-1.57-17.3-1.33-9.47-3.43-16.27-6.56-19.7-3.76-4.11-8.93-3.08-16 4.32-5.65 5.9-10.54 9.5-15.25 11.5-3.58 1.53-6.13 1.99-11.6 2.44-8.8.72-13.17 2.18-20.2 8.1-5.9 4.96-19.5 18.7-21 19.99-6.52 5.68-12.47 8.12-21.32 8.12-19.78 0-29.5-4.42-34.46-16.3-2.49-5.97-9.71-14.38-16.2-18.79-3.42-2.32-6.36-3.35-8.4-2.86-2.2.53-3.44 2.92-3.44 7.45 0 4.28-.16 7.47-.5 11.31l-.23 2.56c-.09.93-.14 1.65-.19 2.35-.24 4.08.06 8.18 1.39 14.78h-1.02zm113.75 0c2.52-3.26 8.93-11.79 10.9-14.3 5.48-6.98 13.05-12.38 19.4-13.94 7.01-1.71 11.5 1.45 11.5 9.24 0 4.02-.04 5.16-.74 19h-1c.7-13.85.74-15 .74-19 0-7.12-3.86-9.83-10.26-8.26-6.11 1.5-13.5 6.77-18.85 13.57-1.86 2.36-7.65 10.07-10.43 13.69h-1.26zm-9.86-338.96c3.44 2.71 7 5.1 11.44 7.75 1.06.64 8.42 4.9 10.35 6.1 11.27 7 15 13.35 12.35 25.33-1.45 6.52-4.53 11.1-9.39 14.44-3.83 2.63-8.07 4.26-16.08 6.56-11.97 3.45-13.68 3.99-18.82 6.28a60.18 60.18 0 0 0-7.81 4.18c-11.11 7.07-19.1 7.7-27.96 3.28-3.56-1.77-17.2-11-17.2-11.01a101.77 101.77 0 0 0-5.2-3.07c-16.04-8.83-34.27-24.16-34.52-31.85-.11-3.46 1.99-6.57 6.28-10.26 1.03-.9 2.18-1.81 3.68-2.95.72-.55 3.38-2.56 3.94-3 4.47-3.4 7.18-5.79 9.32-8.45 11.12-13.82 26.55-28.68 34.36-32.28 12.06-5.54 19.84-5.77 27.37.12 3.25 2.54 5.65 6.54 8.58 13.35.29.65 2.3 5.45 2.88 6.74 1.62 3.65 2.9 5.8 4.24 6.94.72.6 1.45 1.2 2.2 1.8zm-3.49-.28c-1.63-1.39-3.03-3.74-4.77-7.65-.58-1.3-2.6-6.12-2.88-6.76-2.81-6.5-5.08-10.3-7.98-12.56-6.83-5.35-13.85-5.15-25.3.12-7.45 3.42-22.7 18.12-33.64 31.72-2.27 2.82-5.08 5.3-9.67 8.79l-3.94 2.98a79.98 79.98 0 0 0-3.59 2.88c-3.87 3.33-5.67 6-5.58 8.69.21 6.64 18.14 21.72 33.48 30.15 1.76.97 3.5 2 5.3 3.13.12.08 13.61 9.22 17.03 10.92 8.22 4.1 15.46 3.52 26-3.18a62.17 62.17 0 0 1 8.07-4.31c5.25-2.35 7-2.9 19.08-6.38 7.8-2.24 11.9-3.82 15.5-6.3 4.44-3.04 7.23-7.18 8.56-13.22 2.44-11.02-.83-16.6-11.45-23.2-1.9-1.18-9.23-5.42-10.32-6.08-4.5-2.69-8.13-5.12-11.64-7.9-.77-.6-1.52-1.21-2.26-1.84zM87.72 241.6c4.3-2.98 7.88-5 12.14-6.95.84-.4 1.73-.78 2.78-1.24l4.37-1.88a164.3 164.3 0 0 0 17.74-8.96 320.67 320.67 0 0 1 27.87-14.5c4.22-1.95 21.89-9.84 21.17-9.52 19.17-8.62 28.1-6.93 49.5 8.05 7.91 5.54 13.24 13.25 16.45 22.66 3.02 8.83 3.76 16.51 3.76 27.75 0 8.32-.66 12.95-3.68 18.97-4.18 8.36-12.3 16.14-25.58 23.47-24.45 13.49-38.83 27.55-52.83 47.84-8.83 12.8-47.76 44.21-65.16 54.15C75.04 413.55 48.89 423.5 31 423.5c-10.05 0-14.67-4.78-14.76-13.37-.07-6.32 2.06-13.73 6.3-24.32 2.95-7.37 2.02-12.9-2.16-22.29-3.19-7.17-3.88-9.14-3.88-12.52 0-3.35 1.87-6.9 5.52-11.07 2.61-3 3.5-3.83 11.9-11.5 5.09-4.66 8.08-7.6 10.7-10.75 9.46-11.36 12.62-19.47 17.9-44.78 3.12-15.05 6.63-20.28 15.12-25.25.8-.47 3.95-2.25 4.7-2.68a76.66 76.66 0 0 0 5.38-3.38zm.56.82a77.63 77.63 0 0 1-5.44 3.43l-4.7 2.67c-8.23 4.82-11.57 9.81-14.65 24.6-5.3 25.45-8.51 33.7-18.1 45.21-2.66 3.19-5.68 6.16-10.8 10.84-8.36 7.64-9.24 8.48-11.82 11.42-3.5 4.01-5.27 7.36-5.27 10.42 0 3.18.68 5.1 3.8 12.12 4.27 9.6 5.24 15.37 2.16 23.07-4.18 10.47-6.29 17.78-6.22 23.93.08 8.06 4.26 12.38 13.76 12.38 17.67 0 43.68-9.9 64.75-21.93 17.28-9.88 56.1-41.2 64.84-53.85 14.08-20.42 28.57-34.59 53.17-48.16 13.12-7.23 21.09-14.87 25.17-23.03 2.92-5.86 3.57-10.35 3.57-18.53 0-11.13-.74-18.73-3.7-27.43-3.15-9.22-8.36-16.75-16.09-22.16-21.13-14.8-29.7-16.42-48.5-7.95.7-.32-16.96 7.56-21.17 9.5-1.7.8-3.3 1.55-4.86 2.3a319.68 319.68 0 0 0-22.93 12.17 165.3 165.3 0 0 1-17.85 9.01l-4.37 1.88c-1.04.45-1.92.84-2.76 1.23a74.56 74.56 0 0 0-11.99 6.86zm-7.6 12.2c7.7-6.25 12.3-8.17 23.68-11.27 6.12-1.67 9.12-2.95 12.31-5.72 3.8-3.3 7.47-4.52 15.86-6.1 2.75-.52 3.67-.7 5.06-1.02 5.48-1.24 9.48-2.93 13.1-5.89 10.42-8.53 25.4-14.11 36.31-14.11 5.33 0 16.77 7.58 25.74 17.16 10.73 11.46 15.96 23.27 12.73 32.5-3.18 9.1-11.39 18.57-23.03 27.86-8.44 6.73-18.36 13-25.22 16.43-3.72 1.86-6.59 4.88-9.77 9.99-.69 1.1-11.1 20.25-16.03 27.83-5.62 8.65-15.4 17.36-30.23 27.96a552.58 552.58 0 0 1-9.2 6.42c-.13.09-6.81 4.65-8.6 5.89-6.47 4.46-10.35 7.35-13.05 9.83-11.64 10.67-37.14 15.54-43.7 8.98-1.96-1.96-2.2-4.06-1.95-10.52.37-9.42-.5-14.5-4.95-20.51a34.09 34.09 0 0 0-7.04-6.92c-3.93-2.95-6.07-6.11-6.56-9.49-.97-6.61 3.87-13.06 14.17-21.69 1.58-1.32 6.67-5.44 7.09-5.78a48.03 48.03 0 0 0 5.23-4.77c4.1-4.63 5.85-9.55 7.8-20.07a501.52 501.52 0 0 0 .8-4.37c.33-1.87.6-3.3.88-4.73.74-3.78 1.5-7.18 2.4-10.63 1-3.78 1.38-5.5 2.36-10.37.6-3.02.93-4.21 1.56-5.47 1.22-2.45 1.27-2.5 12.25-11.42zm.64.78c-10.77 8.74-10.88 8.84-12 11.08-.58 1.16-.88 2.3-1.47 5.22-.98 4.89-1.36 6.63-2.37 10.44-.9 3.43-1.65 6.8-2.39 10.56a339.79 339.79 0 0 0-1.29 6.95l-.39 2.15c-1.98 10.68-3.77 15.74-8.04 20.54a48.77 48.77 0 0 1-5.34 4.88c-.42.34-5.5 4.47-7.07 5.78-10.04 8.4-14.72 14.65-13.83 20.78.45 3.1 2.44 6.03 6.17 8.83 3 2.25 5.39 4.62 7.24 7.12 4.63 6.24 5.52 11.52 5.15 21.15-.25 6.14-.01 8.1 1.66 9.78 6.1 6.1 31.02 1.33 42.31-9.02 2.75-2.52 6.66-5.43 13.16-9.92l8.6-5.89c3.63-2.48 6.45-4.44 9.19-6.4 14.73-10.54 24.44-19.18 29.97-27.7 4.9-7.54 15.31-26.68 16.02-27.8 3.27-5.26 6.26-8.41 10.18-10.37 6.79-3.4 16.65-9.63 25.03-16.32 11.52-9.18 19.61-18.53 22.72-27.4 3.07-8.78-2.02-20.27-12.52-31.49-8.8-9.4-20.04-16.84-25.01-16.84-10.67 0-25.43 5.5-35.68 13.89-3.76 3.07-7.9 4.81-13.5 6.09-1.41.32-2.35.5-5.11 1.02-8.21 1.55-11.76 2.73-15.38 5.88-3.34 2.9-6.45 4.22-12.7 5.92-11.26 3.07-15.75 4.94-23.31 11.09zM212 251.85c0 7.56-.6 10.92-2.6 14.3-1.1 1.84-7.66 10.05-8.6 11.3-5.96 7.94-9.33 10.28-17.26 13.76-1.34.58-2.2 1-3.03 1.5-.55.33-1.2.66-2 1.02-.71.33-4.46 1.9-5.52 2.39-6.05 2.78-8.99 5.8-8.99 10.73 0 10.97-18.95 36.12-34.51 44.87-8.18 4.6-21.3 9.36-32.78 11.86-13.33 2.9-22.49 2.48-24.62-2.32-1.32-2.97-4.4-4.26-11.98-5.81l-.6-.12c-4.84-.99-6.94-1.55-9.03-2.64-2.92-1.5-4.48-3.7-4.48-6.84 0-2.74 1.08-5.77 3.25-9.67.85-1.53 1.82-3.13 3.23-5.35-.16.25 2.83-4.4 3.67-5.76 6.69-10.7 9.85-18.5 9.85-27.22 0-18.41 11.22-33.37 27.5-42.86 5.22-3.05 9.23-3.31 15.2-2.12 5.04 1 6.05.9 7.43-1.52 4.5-7.85 7.04-9.5 15.87-9.5 3.93 0 6.97-.98 10.47-3.16 1.56-.97 8.67-6.17 10.99-7.68 9.2-5.98 11.34-7 25.2-11.95 6.95-2.48 15.18 1.28 22.33 9.12 6.55 7.19 11.01 16.61 11.01 23.67zm-2 0c0-6.5-4.25-15.48-10.49-22.32-6.67-7.32-14.16-10.74-20.17-8.59-13.73 4.9-15.73 5.85-24.8 11.75-2.24 1.46-9.37 6.68-11.01 7.7-3.8 2.36-7.2 3.46-11.53 3.46-8.08 0-9.98 1.23-14.13 8.5-1.1 1.91-2.51 2.88-4.35 3.09-1.3.14-1.9.05-5.22-.61-5.53-1.1-9.07-.88-13.8 1.88-15.72 9.17-26.5 23.55-26.5 41.14 0 9.2-3.28 17.29-10.15 28.28l-3.68 5.77c-1.39 2.19-2.35 3.77-3.17 5.25-2.02 3.63-3 6.38-3 8.7 0 4.19 2.87 5.67 11.9 7.52l.61.12c8.27 1.7 11.7 3.13 13.4 6.95 3.17 7.14 36 0 54.6-10.46 14.98-8.43 33.49-32.99 33.49-43.13 0-5.9 3.47-9.48 10.16-12.55 1.1-.5 4.85-2.08 5.52-2.38.74-.34 1.32-.64 1.8-.93.92-.55 1.85-1 3.25-1.62 7.65-3.35 10.75-5.5 16.47-13.12 1.02-1.36 7.47-9.42 8.47-11.11 1.79-3.01 2.33-6.06 2.33-13.3zm-37.18-22.4c.15-.1 2.4-1.51 2.95-1.84.96-.57 1.7-.94 2.43-1.17 2.57-.83 5.06-.1 11.04 3.12 14.86 8 19.43 22.87 9.18 38.71-4.04 6.24-9.37 9-18.72 11.11-.85.2-1.2.27-3.13.68-6.04 1.29-8.78 2.08-11.6 3.65-3.63 2.02-6.09 4.98-7.5 9.44-7.87 24.93-19.72 43.34-36.28 50.31-16.45 6.93-21.13 8.53-27.98 8.89-4.94.25-9.8-.65-15.4-2.89a44.45 44.45 0 0 1-5.64-2.6c-4.02-2.33-5.14-4.74-4.5-9.31.3-2.13 3.77-15.53 4.84-20.65.63-3.05 1.19-6.14 1.75-9.69a464.04 464.04 0 0 0 1.35-8.9c1.42-9.41 2.5-14.27 4.49-18.65 2.46-5.43 6.13-9.03 11.72-11.13 6.59-2.47 10.54-3.1 18.03-3.53 4.75-.27 6.68-.64 9-2.05.61-.37 1.22-.81 1.82-1.33a30.61 30.61 0 0 0 3.37-3.4c.59-.69 2.38-2.9 2.63-3.19 3.36-4 6.3-5.53 12.33-5.53 3.94 0 5.9-.92 8.18-3.36-.17.18 2.75-3.14 3.85-4.22a30.95 30.95 0 0 1 6.79-5c1.5-.83 3.15-1.62 4.99-2.38a64.92 64.92 0 0 0 10.01-5.1zm-14.52 8.34a29.95 29.95 0 0 0-6.57 4.84 116.68 116.68 0 0 0-3.82 4.2c-2.46 2.63-4.68 3.67-8.91 3.67-5.72 0-8.39 1.39-11.57 5.17-.23.28-2.03 2.5-2.63 3.2a31.6 31.6 0 0 1-3.47 3.51c-.65.55-1.3 1.03-1.96 1.43-2.5 1.51-4.55 1.9-9.47 2.19-7.39.42-11.25 1.04-17.72 3.47-5.34 2-8.82 5.4-11.17 10.6-1.93 4.27-3 9.07-4.41 18.39l-.65 4.34-.7 4.57c-.57 3.56-1.12 6.67-1.76 9.73-1.08 5.18-4.54 18.53-4.83 20.59-.59 4.17.35 6.18 4.01 8.3 1.35.77 3.1 1.58 5.52 2.55 5.46 2.18 10.18 3.05 14.97 2.8 6.69-.34 11.32-1.93 27.65-8.8 16.21-6.83 27.92-25.01 35.71-49.7 1.49-4.7 4.12-7.86 7.97-10 2.93-1.63 5.74-2.45 11.87-3.76 1.92-.4 2.28-.49 3.12-.68 9.12-2.06 14.24-4.7 18.1-10.67 9.92-15.34 5.55-29.55-8.82-37.29-5.75-3.1-8.03-3.76-10.25-3.05-.65.2-1.33.54-2.23 1.08-.55.32-2.77 1.72-2.93 1.82a65.91 65.91 0 0 1-10.16 5.17c-1.8.75-3.42 1.52-4.89 2.33zm-42.39 32.72c16.15-2.87 26.36-.97 32.47 6.16 5.08 5.93 1.13 21.42-5.93 35.55-4.79 9.58-10.6 16.21-23.16 25.19-14.15 10.1-35.5 12.2-40.71 3.85-1.86-2.97-2.1-8.14-1.06-15.73.78-5.68 1.86-10.71 4.73-22.98l.12-.51c1.59-6.8 2.37-10.31 3.14-14.14 1.45-7.25 3.74-11.47 7.26-13.74 2.81-1.8 5.53-2.28 12.33-2.62 5.33-.27 7.56-.46 10.81-1.03zm.18.98c-3.3.59-5.56.78-10.94 1.05-6.62.33-9.23.78-11.84 2.46-3.25 2.1-5.42 6.09-6.82 13.1-.77 3.84-1.56 7.35-3.15 14.17l-.12.5c-2.86 12.24-3.93 17.26-4.7 22.9-1.03 7.36-.79 12.36.9 15.07 4.82 7.7 25.54 5.67 39.29-4.15 12.43-8.88 18.13-15.39 22.84-24.81 6.86-13.72 10.75-29 6.07-34.45-5.84-6.81-15.7-8.65-31.53-5.84zM132 276.5c7.12 0 10.66 3.08 11.25 8.7.42 4.02-.43 8.14-2.77 15.94-2.56 8.52-18.36 25.38-27.2 31.28-7.01 4.67-20.02 5.67-26.57.99-3.99-2.85-3.53-12.08.02-26.46.68-2.75 1.47-5.65 2.37-8.76a412.6 412.6 0 0 1 3.05-10.14l.37-1.2c1.48-4.8 5.1-7.75 10.73-9.27 4.4-1.2 9.54-1.5 17.48-1.33l3.89.1c3.87.11 5.42.15 7.38.15zm0 1c-1.97 0-3.53-.04-7.41-.15l-3.88-.1c-7.85-.17-12.92.13-17.2 1.3-5.32 1.43-8.67 4.16-10.03 8.6a1277.83 1277.83 0 0 1-1.6 5.21c-.68 2.2-1.27 4.17-1.82 6.1-.9 3.1-1.68 5.99-2.36 8.73-3.43 13.88-3.87 22.93-.4 25.4 6.17 4.42 18.73 3.45 25.42-1 8.66-5.78 24.33-22.49 26.8-30.73 2.3-7.67 3.14-11.71 2.73-15.56-.53-5.1-3.64-7.8-10.25-7.8zm-17.79 7a31.3 31.3 0 0 1 8.57 1.4c5.42 1.78 8.72 5.03 8.72 10.1 0 9.59-9.51 17.2-22.34 21.47-9.82 3.28-13.62-1.79-11.66-16.54.84-6.28 3.82-10.67 8.24-13.46a20.38 20.38 0 0 1 8.47-2.97zm-.6 1.08a19.39 19.39 0 0 0-7.34 2.73c-4.18 2.64-6.98 6.78-7.77 12.76-1.89 14.11 1.36 18.45 10.34 15.46C121.3 312.37 130.5 305 130.5 296c0-4.56-2.98-7.5-8.03-9.15a28.05 28.05 0 0 0-8.2-1.35c-.13 0-.35.03-.66.08zm80.87-23.45c-2.72 9.8-14.93 9.86-26.72 3.3-10.17-5.64-13.8-17.98-5-22.87a66.53 66.53 0 0 0 4.48-2.7l2.03-1.3a50.15 50.15 0 0 1 3.92-2.3c4.73-2.43 8.82-2.8 14-.72 9.16 3.66 10.98 13.33 7.3 26.6zm-20.83-24.98a49.26 49.26 0 0 0-3.84 2.25l-2.03 1.3c-.84.53-1.5.95-2.16 1.35-.82.5-1.6.96-2.38 1.39-7.94 4.4-4.59 15.8 5 21.12 11.31 6.29 22.8 6.23 25.28-2.7 3.57-12.83 1.85-21.97-6.7-25.4-4.9-1.95-8.69-1.62-13.17.7zm17.85 12.15c0 5.7-2.44 9-6.64 9.96-3.3.76-7.56-.05-11.08-1.81l-1.89-.94c-.67-.34-1.18-.62-1.63-.88-4.07-2.38-4.13-4.97.34-10.93 6.8-9.06 20.9-7.16 20.9 4.6zm-1 0c0-5.3-2.87-8.55-7.32-9.16-4.23-.57-8.99 1.44-11.78 5.16-4.15 5.54-4.1 7.44-.64 9.47.44.25.93.51 1.59.85l1.87.93c3.34 1.67 7.36 2.44 10.42 1.74 3.73-.86 5.86-3.74 5.86-9zM387 530.3c0-12.8 2.44-16.74 18.48-29.77a56.8 56.8 0 0 1 7.61-5.2c2.6-1.5 5.33-2.82 8.5-4.18 1.24-.53 2.48-1.05 4.1-1.7l3.92-1.57c9.4-3.83 13.74-6.7 16.62-12.05 1.2-2.22 2.21-4.4 3.23-6.83a148.57 148.57 0 0 0 1.54-3.84l.3-.74.56-1.44c3.2-8.02 6.05-12.08 12.7-16.5a35.26 35.26 0 0 0 4.96-4 46.36 46.36 0 0 0 3.88-4.29c.27-.34 2.55-3.2 3.2-3.98 3.48-4.15 6.51-5.9 11.51-5.9 3.08 0 5.62-.63 9.57-2.1 5.42-2.02 6.53-2.34 8.96-2.2 2.53.13 4.85 1.26 7.18 3.59 1.3 1.3 5.55 5.83 6.52 6.78 5.06 5 9.44 6.92 17.77 6.92a197.5 197.5 0 0 1 12.08.45c15.93.87 21.94.57 25.28-2.21 6.91-5.77 11.64-2.73 11.64 7.76 0 10.73-8.6 20-19 20-4.8 0-8.32 1.43-9.34 3.67-1.12 2.48.68 6.15 5.98 10.57 13.6 11.33 11.24 20.76-7.64 20.76a21.91 21.91 0 0 0-14.6 5.24c-3.28 2.71-5.8 5.86-9.85 11.82l-1.52 2.25c-3.1 4.57-5.01 7.1-7.32 9.4-6.21 6.21-9.3 7.64-13.05 6.89l-1-.23a10.82 10.82 0 0 0-2.66-.37c-1.6 0-2.41.67-8.18 6.22-4.85 4.67-8.07 6.78-11.82 6.78-1.33 0-3.46 1.15-6.45 3.45-1.27.98-2.68 2.14-4.5 3.7l-4.92 4.29a181.11 181.11 0 0 1-4.54 3.82c-9.33 7.56-15.63 10.2-20.21 6.52-2.7-2.15-4.14-4.51-4.63-7.26-.37-2.04-.26-3.63.29-7.3.87-5.85.65-8.42-1.83-11.6-2.32-2.98-2.96-3.22-3.77-2.39-.25.26-1.35 1.63-1.61 1.94-2.21 2.5-4.85 3.57-9 2.82-4.6-.84-5.57-4.11-4.72-10.09l.24-1.56c.6-3.66.68-4.93.25-5.8-.44-.86-1.9-.94-5.23.4l-.74.29c-13.78 5.54-15.26 6.09-19.43 6.67-6.03.84-9.31-1.6-9.31-7.9zm2 0c0 5 2.14 6.6 7.04 5.92 3.91-.55 5.43-1.1 18.95-6.55l.75-.3c4.17-1.66 6.7-1.54 7.76.58.71 1.43.62 2.76-.06 7l-.24 1.53c-.72 5.04-.06 7.27 3.09 7.84 3.43.62 5.38-.17 7.15-2.18.2-.23 1.34-1.66 1.68-2 1.9-1.96 3.82-1.25 6.78 2.55 2.9 3.74 3.17 6.77 2.22 13.12-1 6.75-.52 9.4 3.62 12.71 3.49 2.8 9.1.45 17.7-6.51 1.35-1.1 2.75-2.28 4.49-3.78l4.93-4.3c1.84-1.58 3.27-2.76 4.58-3.77 3.34-2.56 5.74-3.86 7.67-3.86 3.04 0 5.95-1.9 10.43-6.22l2.46-2.39c.94-.89 1.67-1.56 2.37-2.13 1.81-1.49 3.3-2.26 4.74-2.26 1.03 0 1.81.13 3.1.42.7.16.71.17.96.21 2.96.6 5.45-.55 11.23-6.33 2.2-2.2 4.06-4.65 7.09-9.11l1.52-2.25c4.15-6.11 6.76-9.37 10.22-12.24a23.9 23.9 0 0 1 15.88-5.7c16.87 0 18.62-7.01 6.36-17.23-5.9-4.92-8.12-9.41-6.52-12.93 1.42-3.12 5.67-4.84 11.16-4.84 9.25 0 17-8.34 17-18 0-8.94-2.88-10.79-8.36-6.23-3.94 3.28-9.98 3.59-26.67 2.68l-1.02-.06c-5.09-.27-7.99-.39-10.95-.39-8.88 0-13.76-2.14-19.18-7.5-1-.98-5.26-5.53-6.53-6.79-1.99-1.99-3.86-2.9-5.87-3-2.03-.12-3.06.18-8.15 2.07-4.15 1.55-6.9 2.22-10.27 2.22-4.33 0-6.84 1.46-9.98 5.2-.63.74-2.89 3.6-3.18 3.95a48.29 48.29 0 0 1-4.04 4.46 37.26 37.26 0 0 1-5.24 4.23c-6.26 4.17-8.9 7.91-11.95 15.58l-.57 1.43-.28.74a531.5 531.5 0 0 1-1.56 3.88 77.49 77.49 0 0 1-3.32 7c-3.16 5.88-7.82 8.97-17.63 12.96l-3.92 1.58c-1.6.64-2.84 1.15-4.05 1.67a79.2 79.2 0 0 0-8.3 4.08 54.8 54.8 0 0 0-7.35 5.02C391.12 514.78 389 518.21 389 530.31zm133.22-79.76c3.06 1.53 6.54 2.02 10.68 1.7 2.53-.2 4.91-.62 8.8-1.49 5.36-1.19 6.33-1.38 8.33-1.54 2.78-.23 4.82.17 6.29 1.4 1.58 1.31 1.96 2.72 1.26 4.22-.66 1.38-1.05 1.74-5.05 5.07-3.53 2.93-5.03 4.83-5.03 7.09 0 7.3 1.29 10.02 7.83 15.62 3.86 3.3 5.93 6.84 5.28 9.62-.75 3.25-4.96 5.02-12.61 5.02-7.18 0-12.7 4.61-20.03 14.68-.5.7-3.96 5.57-4.94 6.87a38.89 38.89 0 0 1-4.72 5.5c-1.06.98-2.09 1.7-3.1 2.15-2.85 1.26-5.05 1.57-9.83 1.74-7.66.27-10.87 1.45-14.98 7.1-1.58 2.17-3.11 4-4.68 5.6a42.87 42.87 0 0 1-8.65 6.69c-.15.08-10.69 6.19-14.8 8.83-3.76 2.42-6.45 2.04-8.22-.77-1.28-2.03-1.9-4.54-2.87-10.35-.84-5.08-1.27-7.08-2.06-8.93-.97-2.3-2.21-3.24-4.02-2.88-6.2 1.24-8.95 1.39-10.98.2-2.37-1.4-3.13-4.62-2.62-10.73.16-1.96-1.04-2.87-3.76-3.04-2.24-.13-4.9.2-9.94 1.12l-.69.12c-7.97 1.45-10.72 1.72-12.72.73-2.91-1.43-1.6-5.27 4.23-12.21 5.48-6.53 10.6-10.81 15.76-13.53 3.74-1.97 5.94-2.65 12.16-4.1 7.29-1.72 10.4-3.51 14.04-9.31 2.96-4.75 10.74-18.62 12.14-20.84 3.59-5.67 6.8-9.1 11.05-11.34 2.6-1.38 4.72-2.82 9.17-6.07l1.38-1.01c7.85-5.72 12.3-7.98 17.68-7.98 4.22 0 6.49 1.36 9.13 4.77.34.43 1.67 2.22 2 2.67.85 1.09 1.6 1.98 2.45 2.83a24.29 24.29 0 0 0 6.64 4.78zm-.44.9c-2.8-1.4-5-3.03-6.92-4.97-.87-.9-1.65-1.81-2.51-2.93-.35-.46-1.68-2.25-2.01-2.67-2.47-3.18-4.46-4.38-8.34-4.38-5.09 0-9.4 2.2-17.09 7.78l-1.38 1.01c-4.49 3.29-6.63 4.74-9.3 6.15-4.06 2.15-7.16 5.45-10.66 11-1.39 2.19-9.16 16.05-12.15 20.82-3.79 6.07-7.13 7.98-14.66 9.75-6.13 1.45-8.27 2.1-11.92 4.02-5.04 2.66-10.05 6.86-15.46 13.3-5.43 6.46-6.53 9.69-4.55 10.66 1.7.84 4.48.57 12.1-.81l.7-.13c5.12-.93 7.82-1.27 10.17-1.12 3.21.2 4.92 1.48 4.7 4.11-.48 5.76.2 8.64 2.13 9.78 1.73 1.02 4.34.88 10.27-.31 2.35-.47 4 .78 5.14 3.47.83 1.95 1.27 4 2.07 8.8l.06.36c.94 5.65 1.55 8.11 2.72 9.98 1.46 2.3 3.52 2.6 6.84.46 4.14-2.66 14.69-8.77 14.81-8.85a41.9 41.9 0 0 0 8.46-6.54 47.89 47.89 0 0 0 4.6-5.48c4.32-5.95 7.81-7.23 15.74-7.5 4.66-.17 6.76-.47 9.46-1.67.9-.4 1.85-1.06 2.84-1.96a38.03 38.03 0 0 0 4.6-5.36c.96-1.3 4.4-6.16 4.93-6.87 7.5-10.31 13.22-15.09 20.83-15.09 7.24 0 11.02-1.6 11.64-4.24.54-2.32-1.36-5.55-4.97-8.64-6.75-5.79-8.17-8.79-8.17-16.38 0-2.67 1.64-4.74 5.39-7.86 3.8-3.17 4.23-3.56 4.78-4.73.5-1.06.25-1.99-.99-3.03-2.23-1.85-4.72-1.65-13.76.36-3.93.87-6.35 1.3-8.94 1.5-4.3.34-7.97-.18-11.2-1.8zm-28-3.9c5.65-2.82 8.96-2.2 12.9 1.37.56.5 2.6 2.47 3.02 2.87 4.2 3.89 8.07 5.71 14.3 5.71 11.37 0 14 1.41 16.1 8.09.26.83 1.35 4.6 1.66 5.62.8 2.63 1.64 5.03 2.7 7.6 2.13 5.17 2.64 8.32 1.72 10.24-.77 1.61-2.1 2.18-5.37 2.79-2.32.43-2.8.53-3.85.85-1.85.58-3.35 1.4-4.6 2.66-1 1-2.02 2.13-3.31 3.66-.6.71-2.91 3.5-3.46 4.14-7.2 8.54-12.43 12.35-19.59 12.35-3.76 0-6.95 1.28-10.59 4-1.84 1.37-11.62 10.31-15.22 13.06a73.09 73.09 0 0 1-8.95 5.88c-4.58 2.54-7.35 3.22-8.98 2.23-1.32-.8-1.65-2.07-1.94-5.5a52.53 52.53 0 0 0-.16-1.81c-.54-4.73-2.24-6.86-7.16-6.86-7.11 0-8.85-1.23-9.73-5.41-.96-4.61-2.1-6.7-6.55-9.67-3.97-2.65-4.31-5.42-1.52-8.22 2-2 4.63-3.5 11.35-6.87 6.61-3.3 9.2-4.8 11.1-6.68a39.09 39.09 0 0 0 5.3-6.48c.98-1.5 1.83-3.04 2.88-5.13l2.12-4.3c.91-1.83 1.72-3.37 2.61-4.98 5.74-10.32 10.37-14.78 23.22-21.2zm-22.34 21.7c-.89 1.59-1.69 3.12-2.6 4.94l-2.11 4.3a52.9 52.9 0 0 1-2.94 5.23 40.08 40.08 0 0 1-5.44 6.63c-2 2-4.62 3.51-11.35 6.87-6.6 3.3-9.2 4.8-11.1 6.69-2.33 2.34-2.08 4.37 1.38 6.67 4.7 3.14 5.96 5.46 6.97 10.3.78 3.7 2.09 4.62 8.75 4.62 5.5 0 7.57 2.57 8.15 7.75.06.5.09.82.17 1.84.25 3.06.55 4.17 1.46 4.72 1.2.74 3.69.13 7.98-2.25a72.09 72.09 0 0 0 8.82-5.8c3.55-2.7 13.34-11.65 15.24-13.07 3.79-2.83 7.18-4.19 11.18-4.19 6.77 0 11.8-3.67 18.83-12l3.45-4.13a60.07 60.07 0 0 1 3.37-3.72 11.72 11.72 0 0 1 5.01-2.91c1.1-.34 1.6-.45 3.97-.89 2.95-.55 4.07-1.02 4.65-2.23.76-1.59.28-4.5-1.74-9.43a84.46 84.46 0 0 1-2.74-7.69c-.31-1.03-1.4-4.8-1.66-5.61-1.95-6.2-4.16-7.39-15.14-7.39-6.5 0-10.61-1.93-14.98-5.98-.44-.4-2.46-2.37-3.01-2.86-3.65-3.3-6.52-3.85-11.79-1.21-12.67 6.33-17.15 10.65-22.78 20.8zm55.86 11.93c-2.98 6.45-16.78 15.26-26.74 15.26-5.33 0-7.56-2.98-7.11-7.86.32-3.48 2.1-7.91 3.93-10.61l1.52-2.32a44.95 44.95 0 0 1 1.88-2.7c3.66-4.8 7.85-7.45 13.62-7.45 9.06 0 15.75 9.52 12.9 15.68zm-.9-.42c2.52-5.47-3.65-14.26-12-14.26-5.4 0-9.33 2.48-12.82 7.06-.6.8-1.17 1.6-1.85 2.64 0 0-1.2 1.87-1.52 2.33-1.74 2.57-3.46 6.85-3.77 10.14-.4 4.33 1.43 6.77 6.12 6.77 9.57 0 23.02-8.58 25.83-14.68zm-69.67 20.74c2.08.18 4.44.81 5.88 1.8 2.12 1.47 2.2 3.6-.26 6.05-5.14 5.15-12.85 4.34-12.85-1.35 0-4.66 3.14-6.84 7.23-6.5zm-.09 1c-3.56-.3-6.14 1.5-6.14 5.5 0 4.58 6.53 5.26 11.15.65 2.03-2.04 1.98-3.43.4-4.52-1.27-.88-3.48-1.47-5.4-1.63zm29.59-225.95c4.64 2.35 17.27 8.24 19.39 9.43a24.14 24.14 0 0 1 7.05 5.64 45.03 45.03 0 0 1 3.75 5.2c2.4 3.78.04 7.66-6.2 11.63-4.97 3.16-12.18 6.3-21.95 9.82-4.84 1.74-19.63 6.68-21.1 7.2-6.59 2.33-14.85.1-25.14-5.86-3.93-2.27-8-5-12.94-8.54-2.23-1.61-9.5-6.99-10.7-7.85a81.21 81.21 0 0 0-8.63-5.7c-4.82-2.6-4.45-6.64.17-12.13 3.27-3.88 4.17-4.67 18.1-16.33a230.2 230.2 0 0 0 8.89-7.74 95.2 95.2 0 0 0 4.72-4.66c5.08-5.43 9.8-6.49 14.97-3.92 2.24 1.1 4.53 2.85 7.43 5.52 1.48 1.37 6.94 6.72 7.98 7.7 5.2 4.91 9.46 8.2 14.2 10.6zm-.46.9c-4.85-2.45-9.18-5.79-14.44-10.76-1.05-1-6.5-6.34-7.97-7.69-2.83-2.61-5.06-4.3-7.2-5.37-4.75-2.36-9-1.4-13.8 3.71a96.18 96.18 0 0 1-4.76 4.71c-2.48 2.3-5.16 4.62-8.92 7.77-13.86 11.6-14.77 12.4-17.98 16.21-4.28 5.08-4.58 8.4-.46 10.61 2.23 1.2 4.9 2.99 8.74 5.77 1.2.87 8.47 6.24 10.7 7.85a154.8 154.8 0 0 0 12.85 8.49c10.06 5.82 18.07 7.98 24.3 5.78 1.48-.52 16.27-5.47 21.1-7.2 9.7-3.5 16.86-6.61 21.75-9.72 5.84-3.71 7.9-7.1 5.9-10.26a44.09 44.09 0 0 0-3.67-5.08 23.16 23.16 0 0 0-6.78-5.42c-2.08-1.16-14.68-7.05-19.36-9.4zm-38.83 8.05c3.11-.37 5.7-.13 8.4.7 2.15.66 2.74.93 8.64 3.77 4.75 2.29 8.39 3.86 13.19 5.56 8.38 2.97 11.32 6.23 8.83 9.76-2.08 2.94-8.04 5.92-17.84 9.18-8.45 2.82-15.48 2.35-21.43-.9-4.65-2.55-8.33-6.5-12.15-12.3-2.9-4.41-2.73-8.2.16-11.06 2.48-2.45 6.87-4.07 12.2-4.7zm.12 1c-5.13.6-9.33 2.16-11.62 4.42-2.53 2.5-2.68 5.77-.02 9.8 3.73 5.68 7.3 9.51 11.8 11.97 5.7 3.11 12.43 3.57 20.62.84 9.59-3.2 15.44-6.12 17.34-8.82 1.94-2.75-.5-5.45-8.35-8.24-4.84-1.72-8.5-3.3-13.28-5.6-5.84-2.81-6.42-3.07-8.5-3.71a18.42 18.42 0 0 0-8-.66zM202.5 500.38c0 4.78-1.45 7.56-4.43 8.93-2.29 1.05-4.55 1.23-10.79 1.2l-1.78-.01c-9.19 0-17-7.65-17-15.5 0-7.59 10.6-10.51 19.74-5.44 2.78 1.55 4.21 1.94 8.57 2.75 4.44.83 5.69 2.27 5.69 8.07zm-1 0c0-5.3-.9-6.34-4.88-7.08-4.45-.83-5.96-1.25-8.86-2.86-8.57-4.76-18.26-2.1-18.26 4.56 0 7.3 7.36 14.5 16 14.5h1.79c6.06.04 8.26-.14 10.36-1.1 2.6-1.2 3.85-3.6 3.85-8.02zm33.33-117.85c3.71-1.31 8.7-2.7 16.1-4.55 2.58-.65 16.53-4.04 20.56-5.05 19.59-4.93 31.55-8.9 38.23-13.35 14.93-9.95 36.87-33.88 43.83-47.8 2.25-4.5 4.65-6.38 7.68-6.25 1.26.06 2.61.45 4.32 1.2a50.81 50.81 0 0 1 3.54 1.7l1.26.63c4.78 2.34 8.38 3.44 12.65 3.44 7.2 0 10.01 3.07 8.35 7.91-1.4 4.06-5.92 8.91-11.1 12.02-8.3 4.98-11.75 17.3-11.75 33.57 0 3.59-1.37 6.28-3.98 8.36-1.98 1.58-4.2 2.6-8.47 4.16l-1.02.37c-4.85 1.75-6.98 2.77-8.68 4.46-5.09 5.1-12.54 7.15-20.35 7.15-1.38 0-2.47.92-3.99 3.1-.29.41-1.32 1.95-1.47 2.18-2.68 3.92-4.93 5.72-8.54 5.72-7.84 0-10.74.93-21.76 6.94-5.18 2.82-8.8 3.58-14.66 3.68-.26 0-.47 0-.92.02-4.82.06-7.12.3-10.51 1.34a73.43 73.43 0 0 0-8.89 3.56c-2.17 1-10.53 5.01-10.23 4.87-7.79 3.7-13.32 5.98-18.9 7.57-12.41 3.55-18.58 2.24-27.42-4.07-2.58-1.85-2.72-4.43-.83-7.62 1.45-2.45 3.9-5.09 8.08-8.97l1.78-1.64c3.92-3.6 4.48-4.11 5.9-5.53 2.32-2.32 3.12-3.5 5.48-7.63 1.93-3.36 3.37-5.11 6.27-7.06 2.3-1.54 5.34-2.98 9.44-4.43zm.34.94c-4.03 1.42-7 2.83-9.22 4.32-2.75 1.85-4.1 3.49-5.96 6.73-2.4 4.2-3.24 5.44-5.64 7.83-1.43 1.44-2 1.96-5.94 5.57l-1.77 1.63c-4.1 3.82-6.52 6.41-7.9 8.75-1.65 2.79-1.54 4.8.55 6.3 8.6 6.14 14.46 7.38 26.57 3.92 5.5-1.57 11-3.84 18.74-7.51-.3.14 8.06-3.88 10.24-4.88a74.3 74.3 0 0 1 9.01-3.6c3.51-1.09 5.89-1.33 10.8-1.4h.91c5.72-.1 9.18-.83 14.2-3.57 11.16-6.08 14.2-7.06 22.24-7.06 3.19 0 5.2-1.6 7.71-5.28l1.48-2.2c1.7-2.43 3-3.52 4.81-3.52 7.57 0 14.78-2 19.65-6.85 1.83-1.84 4.04-2.9 9.04-4.7l1.02-.37c8.6-3.13 11.79-5.67 11.79-11.58 0-16.6 3.53-29.2 12.24-34.43 5-3 9.35-7.67 10.66-11.48 1.42-4.13-.83-6.59-7.4-6.59-4.45 0-8.19-1.14-13.09-3.54-7.52-3.67-6.78-3.34-8.72-3.43-2.58-.1-4.65 1.52-6.74 5.7-7.04 14.07-29.1 38.14-44.17 48.19-6.81 4.54-18.84 8.52-38.55 13.48-4.03 1.02-17.98 4.4-20.56 5.05-7.37 1.84-12.33 3.23-16 4.52zM252 387.5c2.08 0 4-.2 7.25-.69 5.22-.77 6.64-.9 8.46-.5 2.52.56 3.79 2.35 3.79 5.69 0 4.05-2.27 7.29-6.62 10.11-3.24 2.1-6.53 3.53-14.15 6.4l-.27.1-2.28.86c-3.04 1.16-5.27 2.52-9.33 5.43l-.8.57c-8.19 5.88-13.35 8.03-23.05 8.03-4.98 0-6.88-2.03-5.75-5.62.87-2.81 3.58-6.56 7.8-11.13 1.26-1.37 2.64-2.8 4.15-4.3 3.17-3.14 11.25-10.61 11.45-10.8.46-.47.93-.89 1.4-1.26 3.38-2.71 5.77-3.08 14.18-2.93 1.65.03 2.63.04 3.77.04zm0 1c-1.15 0-2.13-.01-3.79-.04-8.18-.14-10.4.2-13.54 2.71-.44.35-.88.74-1.32 1.18-.2.21-8.3 7.69-11.45 10.82a134.6 134.6 0 0 0-4.12 4.26c-4.12 4.47-6.76 8.12-7.58 10.75-.9 2.88.45 4.32 4.8 4.32 9.46 0 14.44-2.07 22.46-7.84l.8-.57c4.13-2.96 6.42-4.36 9.56-5.56l2.3-.86.25-.1c7.55-2.84 10.8-4.25 13.97-6.3 4.08-2.65 6.16-5.6 6.16-9.27 0-2.89-.97-4.26-3-4.7-1.65-.37-3.05-.25-8.1.5-3.3.5-5.26.7-7.4.7zm112.47-45.34c-1.88 5.44-1.98 6.76-.98 12.76 1.18 7.06-1.38 16.58-5.49 16.58a16.89 16.89 0 0 0-1.51.07l-.64.04c-2.86.18-4.83.17-6.94-.17-6.55-1.06-10.41-5.14-10.41-13.44 0-13.9 2.14-19.69 8.13-26.33a21.9 21.9 0 0 0 2.52-3.75c.59-1.03 2.78-5.13 2.72-5.01 4.44-8.14 7.71-11.53 12.25-10.4 1.17.3 2.2.77 3.58 1.59l1.39.84a20 20 0 0 0 3.1 1.6c.7.27 1.8.32 4.75.26l.72-.01c3.16-.05 4.78.08 5.83.66 1.61.89 1.2 2.56-1.14 4.9a215.9 215.9 0 0 1-3.86 3.76c-10.6 10.1-12.75 12.4-14.02 16.05zm-.94-.32c1.34-3.9 3.46-6.17 14.27-16.46 1.55-1.47 2.73-2.62 3.85-3.73 1.94-1.95 2.17-2.88 1.35-3.33-.82-.45-2.37-.58-5.32-.53l-.72.01c-3.14.06-4.26.02-5.14-.34-1.06-.41-1.97-.9-3.25-1.67l-1.38-.83a12.1 12.1 0 0 0-3.31-1.47c-3.88-.97-6.92 2.17-11.13 9.9.07-.13-2.14 3.98-2.73 5.02a22.71 22.71 0 0 1-2.65 3.92c-5.81 6.47-7.87 12-7.87 25.67 0 7.79 3.48 11.47 9.57 12.45 2.01.33 3.92.34 6.71.16a371.33 371.33 0 0 0 1.23-.07c.42-.03.73-.04.99-.04 3.2 0 5.6-8.9 4.5-15.42-1.02-6.16-.91-7.64 1.03-13.24zm-9.26 12.42c.58.52 2.5 1.9 2.55 1.93 1.96 1.57 2.04 3.31.01 6.36-3.74 5.64-8.83 3.09-8.83-4.55 0-3.81.51-5.67 2.07-6.02 1.18-.26 2 .3 4.2 2.28zm-1.34 1.48c-1.5-1.35-2.23-1.85-2.43-1.8-.17.03-.5 1.23-.5 4.06 0 5.87 2.67 7.21 5.17 3.45 1.5-2.26 1.47-2.84.4-3.7.03.03-1.95-1.4-2.64-2zm222.9-130.19c2.2-1.1 3.67-1.66 5.88-2.36l.28-.09a48.92 48.92 0 0 0 8.79-3.55c4.17-2.08 6.35-1.88 6.96.84.44 2 .2 4.01-1.25 12.7-2.27 13.62-9.16 26.14-21.17 36.3-4.3 3.63-7.41 4.39-9.75 2.44-1.88-1.57-3.1-4.57-4.61-10.48-.3-1.15-1.43-5.83-1.72-6.96a114.18 114.18 0 0 0-2.71-9.22c-2.4-6.82-3.03-10.78-2.1-12.94.77-1.83 2.08-2.24 5.6-2.45 1.49-.09 2.09-.14 2.97-.28l1.95-.33c.72-.12 1.22-.2 1.68-.29 1.1-.2 1.92-.38 2.71-.6 1.7-.49 3.42-1.2 6.49-2.73zm.44.9c-3.11 1.54-4.88 2.29-6.65 2.79-.84.23-1.69.42-2.81.63a108.77 108.77 0 0 1-3.81.63c-.77.13-1.39.19-2.92.28-3.13.18-4.17.51-4.74 1.85-.78 1.84-.2 5.62 2.13 12.2a115.12 115.12 0 0 1 2.74 9.31l1.72 6.96c1.46 5.7 2.62 8.58 4.28 9.96 1.87 1.56 4.49.93 8.47-2.44 11.82-10 18.6-22.3 20.83-35.7 1.4-8.45 1.65-10.51 1.25-12.31-.41-1.87-1.86-2-5.54-.16a49.87 49.87 0 0 1-8.93 3.6l-.28.1a35.4 35.4 0 0 0-5.74 2.3zm-4.5 6.58c1.37-.32 2.5-.75 3.9-1.42.35-.18 2.57-1.31 3.32-1.67 1.5-.71 2.97-1.31 4.7-1.89 2.7-.9 4.64-.77 5.88.4.98.94 1.34 2.26 1.41 4.18.02.4.02.7.02 1.37 0 5.63-4.63 16.88-11.34 22.75-4.34 3.8-7.31 4.67-9.92 2.52-2.06-1.7-3.5-4.65-6.67-12.91-1.86-4.83-2.05-8.1-.68-10.2 1.12-1.7 2.9-2.36 5.83-2.7l1.26-.12c1.19-.12 1.75-.19 2.3-.31zm-2.1 2.3l-1.22.12c-2.4.27-3.7.76-4.39 1.81-.93 1.43-.78 4.1.87 8.38 3.02 7.84 4.41 10.71 6.08 12.09 1.63 1.34 3.64.75 7.33-2.48C584.6 250.77 589 240.08 589 235c0-.64 0-.93-.02-1.29-.05-1.44-.3-2.33-.79-2.8-.6-.57-1.8-.65-3.87.04a37.95 37.95 0 0 0-4.47 1.8c-.72.34-2.93 1.47-3.32 1.66a19.54 19.54 0 0 1-4.3 1.56c-.66.16-1.28.24-2.56.36zm-227.73-88.98c-1.59 4.3-3.54 7.25-7.14 11.4l-2.6 2.97a67.02 67.02 0 0 0-2.63 3.23 46.4 46.4 0 0 0-4.68 7.5c-2.85 5.7-7.14 10.18-12.85 13.89-4.25 2.76-8.25 4.62-15.67 7.59-11.01 4.4-16.43 1.26-27.22-16.4-2.86-4.69-8.8-8.63-17.98-12.66-3-1.33-12.88-5.24-14.43-5.92-4.96-2.18-7.04-3.72-6.42-5.85.67-2.32 5.3-4.05 15.48-6.08 16.63-3.32 26.93-3.82 39.93-3.02 7.9.49 9.67.5 12.74-.26 1.99-.48 3.92-1.3 6-2.6l2.79-1.71c9.86-6.14 12.94-7.96 17.3-9.9 6.03-2.71 10.57-3.32 13.94-1.4 7.2 4.12 7.68 7.7 3.44 19.22zm-1.88-.7c3.95-10.7 3.6-13.26-2.56-16.78-2.66-1.52-6.62-.99-12.12 1.48-4.24 1.9-7.3 3.7-17.07 9.77l-2.79 1.73a22.6 22.6 0 0 1-6.57 2.84c-3.36.81-5.22.8-13.34.3-12.84-.78-22.97-.29-39.41 3-4.9.97-8.45 1.88-10.79 2.75-2.03.76-3.04 1.45-3.17 1.91-.16.57 1.48 1.79 5.3 3.46 1.5.67 11.39 4.58 14.44 5.93 9.52 4.19 15.74 8.3 18.87 13.44 10.35 16.93 14.87 19.56 24.78 15.6 7.3-2.93 11.21-4.75 15.33-7.42 5.42-3.53 9.47-7.75 12.15-13.1 1.44-2.9 3.02-5.4 4.86-7.82a68.95 68.95 0 0 1 2.72-3.33l2.6-2.97c3.46-3.99 5.28-6.75 6.77-10.79zm-6.64-.39c-7.94 12.8-18.53 21.75-33.3 25.23-7.82 1.83-12.47-.79-13.12-5.93-.55-4.45 2.29-9.06 6-9.06 3.02 0 5.6-1.68 15.38-9.16 1.47-1.12 2.57-1.96 3.66-2.74 4.4-3.2 7.77-5.17 10.82-6.08 5.57-1.67 9.33-2.15 11.35-1.22 2.5 1.14 2.22 4.13-.79 8.96zm-.84-.52c2.72-4.4 2.94-6.74 1.21-7.53-1.71-.79-5.32-.33-10.65 1.27-2.9.87-6.2 2.79-10.51 5.92-1.08.79-2.18 1.62-3.65 2.74-10.08 7.72-12.62 9.36-15.98 9.36-3.02 0-5.5 4.02-5 7.94.56 4.5 4.62 6.78 11.89 5.07 14.48-3.4 24.86-12.18 32.69-24.77zM461.17 33.53c13.88 4.96 20.75 4.96 31.62.01 3.02-1.37 5.47-2.94 11-6.82 5.57-3.92 8.05-5.51 11.14-6.92 4.14-1.88 7.78-2.38 11.22-1.28 3.92 1.26 6.2 12.3 6.78 28.45.5 14.2-.52 28.93-2.46 34.2-1.82 4.93-5.86 8.17-11.51 10.02A41.7 41.7 0 0 1 506 93.01c-5.79 0-9 2.4-12.2 7.64-.37.59-1.55 2.6-1.71 2.87-1.75 2.9-3.05 4.33-4.93 4.95-.94.32-2.07.83-3.87 1.74l-2.43 1.23c-1.03.53-1.87.94-2.7 1.34-6.43 3.1-11.73 4.72-17.16 4.72-5.71 0-10.04 2.09-14.02 5.92-1.16 1.11-4.2 4.53-4.63 4.94-2.54 2.44-5.93 4.24-10.85 6.1-1.4.52-5.98 2.13-6.25 2.22l-2.06.78c-.89.36-1.78.63-2.7.81-5.55 1.14-11.14-.54-17.98-4.42-1.27-.73-5.13-3.06-5.76-3.42-2.05-1.16-4.12-1.53-9.09-1.9l-1.73-.15c-4.78-.4-7.68-1.14-10.22-2.97-5-3.61-6.77-7.76-5.65-12.33 1.33-5.42 6.5-11.02 14.85-17.28a169.2 169.2 0 0 1 6.5-4.61c-.33.23 4.33-2.92 5.3-3.6 2.73-1.91 4.8-3.9 12.75-12.04l1.09-1.1c3.49-3.56 5.89-5.89 8.12-7.83 2.9-2.5 4.72-5.95 7.5-13.05l.63-1.61c2.7-6.92 4.28-10 6.87-12.33 1.42-1.28 6.68-6.54 7.93-7.5 3.98-3 8.01-2.73 19.57 1.4zm-.34.94c-11.26-4.02-15-4.28-18.62-1.53-1.19.9-6.4 6.11-7.88 7.43-2.42 2.18-3.96 5.19-6.6 11.95l-.63 1.61c-2.83 7.26-4.72 10.8-7.77 13.45a141.85 141.85 0 0 0-9.16 8.87c-8.02 8.2-10.08 10.2-12.88 12.16-.99.69-5.65 3.84-5.31 3.6-2.5 1.71-4.52 3.13-6.47 4.59-8.17 6.13-13.23 11.6-14.48 16.72-1.02 4.15.58 7.9 5.26 11.27 2.36 1.7 5.11 2.4 9.72 2.8l1.73.13c5.12.4 7.28.78 9.5 2.05.65.36 4.5 2.7 5.76 3.4 6.66 3.78 12.04 5.4 17.29 4.32.86-.17 1.7-.42 2.52-.75a67 67 0 0 1 2.1-.8c.28-.1 4.86-1.7 6.24-2.22 4.8-1.8 8.08-3.56 10.5-5.88.4-.38 3.44-3.8 4.63-4.94 4.16-4 8.72-6.2 14.72-6.2 5.25 0 10.42-1.59 16.73-4.62.82-.4 1.65-.8 2.68-1.33.12-.06 1.93-.99 2.43-1.23 1.84-.93 3-1.46 4-1.8 1.6-.52 2.76-1.82 4.39-4.52l1.7-2.88c3.39-5.5 6.87-8.11 13.07-8.11 4.45 0 8.73-.49 12.64-1.77 5.4-1.76 9.2-4.8 10.9-9.41 1.87-5.11 2.9-19.75 2.39-33.83-.56-15.53-2.81-26.48-6.08-27.52-3.18-1.02-6.57-.55-10.5 1.23-3.02 1.37-5.47 2.94-11 6.83-5.57 3.92-8.05 5.5-11.14 6.92-11.13 5.05-18.26 5.05-32.38.01zM475 55c5.38 0 7.55-.21 9.72-.96 1.26-.43 9.95-4.8 14.88-6.96 1.9-.82 3.56-2.44 6.6-6.04 2.56-3.04 3.19-3.75 4.4-4.84 3.7-3.35 7.07-3.28 10.22 1.23 6.23 8.9 5.61 15.94.07 27.02a71.26 71.26 0 0 0-2.5 5.48c-.32.8-1 2.7-1.09 2.9-.17.45-.34.81-.54 1.17-.63 1.14-1.56 2.21-4.05 4.7-2.4 2.4-5.16 3.27-11.68 4.33-1.81.3-2.2.36-3 .51-6.02 1.1-9.6 2.69-12.24 6.07-3.57 4.59-7.9 7.48-14.98 10.74-.55.24-1.1.5-1.8.8l-1.78.8a60.08 60.08 0 0 0-7.7 3.9c-2.57 1.6-4.79 2.35-9.42 3.46-8.58 2.06-12.28 3.76-17.37 9.36-5.12 5.64-10.17 7.64-16.63 6.7-5.36-.79-10.63-3.01-23.56-9.48-6.3-3.15-6.43-7.78-1.5-13.56 3.38-3.94 3.52-4.06 19.4-16.44 8.12-6.33 12.97-10.57 16.63-14.88 2.53-2.98 4.2-5.73 4.96-8.3 5.5-18.3 12.5-21.98 22.78-15.56 1.95 1.22 6.61 4.55 7.18 4.9 3.36 2.15 6.52 2.95 13 2.95zm0 2c-6.84 0-10.37-.89-14.08-3.26-.63-.4-5.27-3.71-7.16-4.9-9.05-5.65-14.66-2.7-19.8 14.45-.86 2.87-2.67 5.85-5.35 9.01-3.78 4.45-8.7 8.75-16.94 15.17-15.66 12.21-15.86 12.38-19.1 16.16-4.17 4.9-4.09 8 .88 10.48 12.71 6.35 17.89 8.54 22.94 9.28 5.78.84 10.18-.9 14.87-6.06 5.42-5.96 9.45-7.82 18.38-9.96 4.43-1.07 6.5-1.76 8.83-3.22a61.7 61.7 0 0 1 7.94-4.02l1.78-.8 1.78-.8c6.82-3.13 10.91-5.87 14.24-10.14 3-3.87 7-5.64 13.46-6.82.83-.15 1.21-.21 3.04-.51 6.1-1 8.6-1.78 10.58-3.77 2.36-2.36 3.21-3.34 3.72-4.26.15-.27.29-.56.44-.94.06-.15.75-2.06 1.09-2.9.64-1.6 1.45-3.4 2.57-5.64 5.24-10.49 5.8-16.8.07-24.98-2.4-3.44-4.37-3.48-7.24-.89-1.11 1-1.73 1.7-4.22 4.65-3.24 3.85-5.04 5.59-7.32 6.59-4.82 2.1-13.62 6.53-15.03 7.01-2.44.84-4.79 1.07-10.37 1.07zm-12.7 8.6c5.47 3.9 10.34 3.72 18.23.88 5.39-1.94 5.92-2.1 7.7-2.1 2.5-.01 4.21 1.36 5.24 4.46 1.66 4.98-2.32 8.52-12.3 12.68-2.7 1.13-16.25 6.18-20 7.73-7.86 3.24-13.93 6.42-18.87 10.15-13.02 9.84-18.36 11.93-23.71 9.68a24.67 24.67 0 0 1-3.62-1.98l-1.99-1.28a90.4 90.4 0 0 0-2.24-1.4c-3.33-2-2.82-4.28.85-7.34 1.35-1.13 10.66-7.61 13.53-9.91 7.1-5.69 11.91-11.47 14.41-18.34 3.07-8.45 4.89-12.1 6.8-13.39 1.73-1.16 3.36-.53 6.18 1.9.63.56 3.4 3.08 4.11 3.7 1.93 1.7 3.71 3.15 5.67 4.55zm-.6.8c-1.98-1.42-3.79-2.88-5.74-4.6-.73-.64-3.48-3.16-4.1-3.7-2.5-2.16-3.75-2.65-4.97-1.83-1.66 1.11-3.44 4.7-6.42 12.9-2.57 7.07-7.5 12.99-14.72 18.78-2.91 2.33-12.21 8.8-13.52 9.9-3.22 2.68-3.56 4.17-.97 5.72l2.26 1.4 1.99 1.28c1.47.93 2.48 1.5 3.47 1.91 4.9 2.07 9.96.07 22.72-9.56 5.02-3.79 11.15-7 19.1-10.28 3.76-1.55 17.3-6.6 20-7.72 9.5-3.97 13.14-7.2 11.73-11.44-.9-2.71-2.25-3.8-4.3-3.79-1.6 0-2.15.17-7.36 2.05-8.17 2.94-13.34 3.14-19.16-1.01z'%3E%3C/path%3E%3C/svg%3E"); - } - position: relative; - padding: 24px; - top: ${headerHeight}px; -`; diff --git a/src/components/home/background-style.ts b/src/components/home/background-style.ts new file mode 100644 index 00000000..d43b256a --- /dev/null +++ b/src/components/home/background-style.ts @@ -0,0 +1,23 @@ +import { css, SerializedStyles, Theme } from "@emotion/react"; +import { headerHeight } from "@styles/constants"; + +export const homeBackground = (theme: Theme): SerializedStyles => css` + &:before { + content: " "; + width: 100vw; + height: 100vh; + top: 0; + left: 0; + position: fixed; + background-color: ${theme.background}; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='600' height='600' viewBox='0 0 600 600'%3E%3Cpath fill='%23${( + theme.altButtonBackground || "" + ).replace( + "#", + "" + )}' fill-opacity='0.4' d='M600 325.1v-1.17c-6.5 3.83-13.06 7.64-14.68 8.64-10.6 6.56-18.57 12.56-24.68 19.09-5.58 5.95-12.44 10.06-22.42 14.15-1.45.6-2.96 1.2-4.83 1.9l-4.75 1.82c-9.78 3.75-14.8 6.27-18.98 10.1-4.23 3.88-9.65 6.6-16.77 8.84-1.95.6-3.99 1.17-6.47 1.8l-6.14 1.53c-5.29 1.35-8.3 2.37-10.54 3.78-3.08 1.92-6.63 3.26-12.74 5.03a384.1 384.1 0 0 1-4.82 1.36c-2.04.58-3.6 1.04-5.17 1.52a110.03 110.03 0 0 0-11.2 4.05c-2.7 1.15-5.5 3.93-8.78 8.4a157.68 157.68 0 0 0-6.15 9.2c-5.75 9.07-7.58 11.74-10.24 14.51a50.97 50.97 0 0 1-4.6 4.22c-2.33 1.9-10.39 7.54-11.81 8.74a14.68 14.68 0 0 0-3.67 4.15c-1.24 2.3-1.9 4.57-2.78 8.87-2.17 10.61-3.52 14.81-8.2 22.1-4.07 6.33-6.8 9.88-9.83 12.99-.47.48-.95.96-1.5 1.48l-3.75 3.56c-1.67 1.6-3.18 3.12-4.86 4.9a42.44 42.44 0 0 0-9.89 16.94c-2.5 8.13-2.72 15.47-1.76 27.22.47 5.82.51 6.36.51 8.18 0 10.51.12 17.53.63 25.78.24 4.05.56 7.8.97 11.22h.9c-1.13-9.58-1.5-21.83-1.5-37 0-1.86-.04-2.4-.52-8.26-.94-11.63-.72-18.87 1.73-26.85a41.44 41.44 0 0 1 9.65-16.55c1.67-1.76 3.18-3.27 4.83-4.85.63-.6 3.13-2.96 3.75-3.57a71.6 71.6 0 0 0 1.52-1.5c3.09-3.16 5.86-6.76 9.96-13.15 4.77-7.42 6.15-11.71 8.34-22.44.86-4.21 1.5-6.4 2.68-8.6.68-1.25 1.79-2.48 3.43-3.86 1.38-1.15 9.43-6.8 11.8-8.72 1.71-1.4 3.26-2.81 4.7-4.3 2.72-2.85 4.56-5.54 10.36-14.67a156.9 156.9 0 0 1 6.1-9.15c3.2-4.33 5.9-7.01 8.37-8.07 3.5-1.5 7.06-2.77 11.1-4.02a233.84 233.84 0 0 1 7.6-2.2l2.38-.67c6.19-1.79 9.81-3.16 12.98-5.15 2.14-1.33 5.08-2.33 10.27-3.65l6.14-1.53c2.5-.63 4.55-1.2 6.52-1.82 7.24-2.27 12.79-5.06 17.15-9.05 4.05-3.72 9-6.2 18.66-9.9l4.75-1.82c1.87-.72 3.39-1.31 4.85-1.91 10.1-4.15 17.07-8.32 22.76-14.4 6.05-6.45 13.95-12.4 24.49-18.92 1.56-.96 7.82-4.6 14.15-8.33v-64.58c-4 8.15-8.52 14.85-12.7 17.9-2.51 1.82-5.38 4.02-9.04 6.92a1063.87 1063.87 0 0 0-6.23 4.98l-1.27 1.02a2309.25 2309.25 0 0 1-4.87 3.9c-7.55 6-12.9 10.05-17.61 13.19-3.1 2.06-3.86 2.78-8.06 7.13-5.84 6.07-11.72 8.62-29.15 10.95-11.3 1.5-20.04 4.91-30.75 11.07-1.65.94-7.27 4.27-6.97 4.1-2.7 1.58-4.69 2.69-6.64 3.66-5.63 2.8-10.47 4.17-15.71 4.17-17.13 0-41.44 11.51-51.63 22.83-12.05 13.4-31.42 27.7-45.25 31.16-7.4 1.85-11.85 7.05-14.04 14.69-1.26 4.4-1.58 8.28-1.58 13.82 0 .82.01.98.24 3.63.45 5.18.35 8.72-.77 13.26-1.53 6.2-4.89 12.6-10.59 19.43-13.87 16.65-22.88 46.58-22.88 71.68 0 2.39.02 4.26.06 8.75.12 10.8.1 15.8-.22 21.95-.56 11.18-2.09 20.73-5 29.3h-1.05c2.94-8.56 4.49-18.12 5.05-29.35.31-6.13.34-11.1.22-21.9-.04-4.48-.06-6.36-.06-8.75 0-25.32 9.07-55.47 23.12-72.32 5.6-6.72 8.88-12.99 10.38-19.03 1.09-4.4 1.18-7.85.74-12.93-.23-2.7-.24-2.86-.24-3.72 0-5.62.32-9.57 1.62-14.1 2.28-7.95 6.97-13.44 14.76-15.39 13.6-3.4 32.82-17.59 44.75-30.84C409 360.14 433.58 348.5 451 348.5c5.07 0 9.77-1.33 15.26-4.07 1.93-.96 3.9-2.05 6.58-3.62-.3.18 5.33-3.16 6.98-4.11 10.82-6.21 19.66-9.67 31.11-11.2 17.23-2.3 22.9-4.75 28.57-10.64 4.25-4.41 5.04-5.16 8.22-7.28 4.68-3.11 10.01-7.14 17.55-13.14a1113.33 1113.33 0 0 0 4.86-3.89l1.28-1.02a4668.54 4668.54 0 0 1 6.23-4.98c3.67-2.9 6.55-5.12 9.07-6.95 4.37-3.19 9.16-10.56 13.29-19.4v66.9zm0-116.23c-.62.01-1.27.06-1.95.13-6.13.63-13.83 3.45-21.83 7.45-3.64 1.82-8.46 2.67-14.17 2.71-4.7.04-9.72-.47-14.73-1.33-1.7-.3-3.26-.61-4.67-.93a31.55 31.55 0 0 0-3.55-.57 273.4 273.4 0 0 0-16.66-.88c-10.42-.16-17.2.74-17.97 2.73-.38.97.6 2.55 3.03 4.87 1.01.97 2.22 2.03 4.04 3.55a1746.07 1746.07 0 0 0 4.79 4.02c1.39 1.2 3.1 1.92 5.5 2.5.7.16.86.2 2.64.54 3.53.7 5.03 1.25 6.15 2.63 1.41 1.76 1.4 4.54-.15 8.88-2.44 6.83-5.72 10.05-10.19 10.33-3.63.23-7.6-1.29-14.52-5.06-4.53-2.47-6.82-7.3-8.32-15.26-.17-.87-.32-1.78-.5-2.86l-.43-2.76c-1.05-6.58-1.9-9.2-3.73-10.11-.81-.4-1.59-.74-2.36-1-2.27-.77-4.6-1.02-8.1-.92-2.29.07-14.7 1-13.77.93-20.55 1.37-28.8 5.05-37.09 14.99a133.07 133.07 0 0 0-4.25 5.44l-2.3 3.09-2.51 3.32c-4.1 5.36-7.06 8.48-10.39 11.12-.65.52-1.33 1.04-2.13 1.62l-4.11 2.94a106.8 106.8 0 0 0-5.16 3.99c-4.55 3.74-9.74 8.6-16.25 15.38-8.25 8.58-11.78 13.54-11.7 15.95.07 1.65 1.64 2.11 6.79 2.38 1.61.09 2.15.12 2.98.2 2.95.24 5.09.73 6.81 1.68 7.48 4.15 11.63 7.26 13.95 11.58 3.3 6.15.8 12.88-8.89 20.26-8.28 6.3-11.1 10.37-11.31 14.96-.06 1.17 0 1.93.26 4.43.69 6.47.25 10.65-2.8 17.42a44.23 44.23 0 0 1-4.16 7.53c-2.82 3.97-5.47 5.74-10.6 7.69-.43.16-3.34 1.23-4.27 1.59-1.8.68-3.38 1.36-5.01 2.14-4.18 2-8.4 4.6-13.1 8.24-8.44 6.51-13.23 14.56-15.98 25.06-1.1 4.2-1.55 6.81-2.8 15.21-1.26 8.6-2.17 12.64-4.08 16.55-2.1 4.28-11.93 26.59-12.97 28.88a382.7 382.7 0 0 1-6.37 13.41c-4.07 8.11-7.61 14.07-10.73 17.81-5.38 6.46-8.98 14.37-13.77 28.42a810.14 810.14 0 0 0-1.89 5.6c-1.8 5.35-2.96 8.6-4.26 11.85-6.13 15.32-25.43 26.31-46.46 26.31-11.2 0-20.58-2.74-31.02-8.55-5.6-3.13-4.55-2.42-22.26-14.54-14.33-9.8-17.7-10.73-20.47-6.9-.37.5-1.81 2.74-1.83 2.77a52.24 52.24 0 0 1-4.94 5.9c-.73.79-5.52 5.87-6.97 7.45-2.38 2.6-4.3 4.81-5.98 6.93a45.6 45.6 0 0 0-5.08 7.66c-1.29 2.57-1.9 5.25-2.66 10.6a997.6 997.6 0 0 1-.46 3.18h-1l.47-3.32c.77-5.45 1.4-8.2 2.75-10.9a46.54 46.54 0 0 1 5.2-7.84c1.7-2.14 3.63-4.38 6.03-6.98 1.45-1.59 6.24-6.68 6.96-7.46a51.58 51.58 0 0 0 4.84-5.78s1.47-2.26 1.86-2.8c3.25-4.5 7.08-3.44 21.84 6.67 17.67 12.08 16.62 11.38 22.19 14.48 10.3 5.73 19.5 8.43 30.53 8.43 20.65 0 39.57-10.77 45.54-25.69a219.7 219.7 0 0 0 4.24-11.8 6752.32 6752.32 0 0 0 1.88-5.6c4.83-14.16 8.47-22.14 13.96-28.73 3.05-3.66 6.56-9.57 10.6-17.61 1.97-3.93 4.04-8.31 6.35-13.38 1.03-2.28 10.88-24.61 12.98-28.91 1.85-3.79 2.75-7.76 4-16.25 1.24-8.44 1.7-11.07 2.81-15.32 2.8-10.7 7.71-18.94 16.33-25.6a73.18 73.18 0 0 1 13.29-8.35c1.66-.8 3.27-1.48 5.08-2.18.94-.36 3.86-1.43 4.28-1.59 4.95-1.88 7.44-3.55 10.14-7.33 1.35-1.9 2.68-4.3 4.06-7.37 2.97-6.58 3.39-10.59 2.72-16.9a27.13 27.13 0 0 1-.27-4.58c.22-4.94 3.21-9.24 11.7-15.7 9.33-7.11 11.66-13.34 8.62-19-2.2-4.09-6.25-7.12-13.55-11.17-1.57-.88-3.6-1.33-6.42-1.57-.8-.07-1.34-.1-2.95-.19-5.77-.3-7.63-.85-7.72-3.34-.1-2.81 3.5-7.87 11.97-16.69 6.53-6.8 11.75-11.69 16.33-15.45 1.79-1.47 3.42-2.72 5.2-4.03l4.12-2.94c.79-.58 1.46-1.08 2.1-1.59 3.26-2.6 6.16-5.65 10.21-10.94a383.2 383.2 0 0 0 2.5-3.32l2.31-3.09c1.8-2.39 3.04-4 4.29-5.48 8.47-10.17 16.98-13.96 37.27-15.3-.44.02 12-.9 14.32-.98 3.62-.1 6.05.16 8.46.98.8.27 1.62.62 2.47 1.04 2.27 1.14 3.17 3.87 4.27 10.85l.44 2.76c.17 1.07.33 1.97.5 2.83 1.44 7.69 3.62 12.29 7.8 14.57 6.76 3.68 10.6 5.15 13.99 4.94 4-.25 6.99-3.17 9.3-9.67 1.45-4.04 1.46-6.49.32-7.92-.9-1.12-2.28-1.62-5.57-2.27a55.8 55.8 0 0 1-2.67-.55c-2.54-.6-4.39-1.4-5.93-2.71a252.63 252.63 0 0 0-4.78-4.01 84.35 84.35 0 0 1-4.08-3.6c-2.73-2.6-3.86-4.43-3.28-5.95 1.02-2.64 7.82-3.54 18.93-3.37a230.56 230.56 0 0 1 16.73.88c2.76.39 3.2.49 3.68.6 1.4.3 2.95.62 4.62.91a82.9 82.9 0 0 0 14.56 1.32c5.56-.04 10.24-.86 13.73-2.6 8.1-4.05 15.89-6.9 22.17-7.56.7-.07 1.4-.11 2.05-.13v1zm0-100.94v1.5c-8.62 16.05-17.27 29.55-23.65 35.92-3.19 3.2-7.62 4.9-13.54 5.56-4.45.48-8.28.4-19.18-.2-9.91-.55-15.32-.44-20.52.78a84.05 84.05 0 0 1-15 2.11l-2.25.14c-12.49.75-19.37 1.78-32.72 5.74-4.5 1.33-9.27 2.49-14.3 3.48a246.27 246.27 0 0 1-32.6 3.97c-7.56.45-13.21.57-20.24.57-5.4 0-11.9 1.61-18 5.18-8.3 4.87-15.06 12.87-19.53 24.5a68.57 68.57 0 0 1-4.56 9.8c-3.6 6.2-6.92 8.99-13.38 12.18l-4.03 1.96a64.48 64.48 0 0 0-15.16 10.25c-8.2 7.33-13.72 16.63-22.54 35.6l-2.08 4.49c-7.3 15.7-11.5 23.3-17.35 29.87-7.7 8.66-20.25 14.42-40.31 20.08-4.37 1.23-19.04 5.08-19.24 5.13-6.92 1.87-11.68 3.34-15.63 4.92-10.55 4.22-18.71 10.52-36.38 26.52l-1.7 1.54c-8.58 7.76-13.41 11.9-18.81 15.88-3.95 2.9-8 5.67-12.97 8.91-2.06 1.34-10.3 6.6-12.33 7.94-11.52 7.5-18.53 13.04-24.62 20.08a62.01 62.01 0 0 0-6.44 8.85c-4.13 6.91-6.27 13.15-9.2 25.11l-1.54 6.26c-.6 2.45-1.15 4.54-1.72 6.58-2.97 10.7-6.9 17.36-14.78 26.91L69.6 491a148.51 148.51 0 0 0-4.19 5.3 23.9 23.9 0 0 0-3.44 6.28c-1.16 3.23-1.52 5.9-1.87 11.94-.58 10.05-1.42 15.04-4.63 22.67-1.57 3.72-5.66 14.02-6.41 15.8a73.46 73.46 0 0 1-3.57 7.4c-2.88 5.14-6.71 10.12-13.12 16.95-5.96 6.36-8.87 10.9-10.61 16a56.88 56.88 0 0 0-1.38 4.82l-.46 1.84h-1.03l.52-2.08c.52-2.09.92-3.49 1.4-4.9 1.8-5.25 4.78-9.9 10.84-16.36 6.35-6.78 10.13-11.7 12.97-16.77a72.5 72.5 0 0 0 3.52-7.29c.75-1.76 4.84-12.06 6.4-15.8 3.17-7.5 3.99-12.4 4.56-22.33.35-6.14.72-8.88 1.93-12.23a24.9 24.9 0 0 1 3.58-6.54c1.27-1.7 2.6-3.37 4.22-5.34l4.11-4.95c7.8-9.46 11.66-16 14.59-26.54.56-2.04 1.1-4.12 1.71-6.56l1.53-6.26c2.96-12.04 5.13-18.36 9.32-25.39 1.84-3.08 4-6.05 6.54-8.99 6.17-7.12 13.24-12.7 24.83-20.26 2.05-1.33 10.28-6.6 12.33-7.94 4.96-3.22 9-5.98 12.92-8.87 5.37-3.95 10.19-8.08 18.74-15.82l1.7-1.54c17.76-16.09 25.98-22.43 36.67-26.7 4-1.6 8.8-3.09 15.75-4.96.21-.06 14.87-3.9 19.22-5.13 19.9-5.61 32.32-11.31 39.85-19.78 5.76-6.48 9.93-14.02 17.18-29.64l2.09-4.5c8.87-19.07 14.44-28.46 22.77-35.9a65.48 65.48 0 0 1 15.38-10.4l4.04-1.97c6.3-3.1 9.47-5.77 12.96-11.77a67.6 67.6 0 0 0 4.48-9.67c4.56-11.84 11.47-20.02 19.97-25 6.25-3.66 12.93-5.32 18.5-5.32 7.01 0 12.65-.12 20.17-.57a245.3 245.3 0 0 0 32.47-3.96c5-.98 9.75-2.13 14.22-3.45 13.43-3.98 20.38-5.02 32.94-5.78l2.24-.14c5.76-.37 9.8-.9 14.85-2.09 5.31-1.25 10.79-1.35 22.6-.7 9.04.5 12.84.58 17.21.1 5.71-.62 9.94-2.26 12.95-5.26 6.44-6.45 15.3-20.37 24.35-36.72zm0 450.21c-1.28-4.6-2.2-10.55-3.33-20.25l-.24-2.04-.23-2.03c-1.82-15.7-3.07-21.98-5.55-24.47-2.46-2.46-3.04-5.03-2.52-8.64.1-.6.18-1.1.39-2.15.69-3.54.77-5.04.08-6.84-.91-2.38-3.31-4.41-7.79-6.26-5.08-2.09-6.52-4.84-4.89-8.44.66-1.45 1.79-3.02 3.52-5.01 1.04-1.2 5.48-5.96 5.08-5.53 6.15-6.7 8.98-11.34 8.98-16.48a15.2 15.2 0 0 1 6.5-12.89v1.26a14.17 14.17 0 0 0-5.5 11.63c0 5.47-2.93 10.29-9.24 17.16.38-.42-4.04 4.33-5.07 5.5-1.67 1.93-2.75 3.43-3.36 4.77-1.37 3.04-.23 5.22 4.36 7.1 4.71 1.95 7.32 4.16 8.34 6.83.78 2.04.7 3.67-.03 7.4-.2 1.03-.3 1.51-.38 2.09-.48 3.33.03 5.59 2.23 7.8 2.74 2.74 3.98 8.96 5.84 25.06l.24 2.03.23 2.04c.82 7.01 1.53 12.06 2.34 16.03v4.33zm0-62.16c-1.4-3.13-4.43-9.9-4.95-11.17-1.02-2.53-1.25-3.8-.91-5.18.2-.84 2.05-4.68 2.32-5.33a70.79 70.79 0 0 0 3.54-11.2v3.99a62.82 62.82 0 0 1-2.62 7.6c-.31.75-2.09 4.46-2.27 5.18-.28 1.12-.08 2.22.87 4.57.41 1.02 2.5 5.7 4.02 9.09v2.45zm0-85.09c-1.65 1.66-3.66 2.9-6.4 4.13-.25.1-13.97 5.47-20.4 8.43-9.35 4.32-16.7 5.9-23.03 5.25-5.08-.53-9.02-2.25-14.77-5.92l-3.2-2.07a77.4 77.4 0 0 0-5.44-3.27c-4.05-2.18-3.25-5.8 1.47-10.47 3.71-3.68 9.6-7.93 18.73-13.8l4.46-2.82c17.95-11.33 18.22-11.5 22.27-14.74 11.25-9 19.69-14.02 26.31-15.1v1.02c-6.37 1.1-14.62 6-25.69 14.86-4.1 3.28-4.34 3.44-22.36 14.8a652.4 652.4 0 0 0-4.45 2.83c-9.07 5.83-14.92 10.05-18.57 13.66-4.31 4.28-4.95 7.13-1.7 8.88 1.7.91 3.29 1.88 5.5 3.3l3.2 2.08c5.64 3.59 9.45 5.25 14.34 5.76 6.13.64 13.32-.9 22.52-5.15 6.46-2.98 20.18-8.35 20.4-8.44 3.04-1.37 5.1-2.71 6.81-4.69v1.47zm0-41.37v1c-6.56.26-12.11 3.13-19.71 9.08l-4.63 3.68a51.87 51.87 0 0 1-4.4 3.14c-.82.52-5.51 3.33-6.22 3.76-3.31 2-6.15 3.8-8.87 5.6a112.61 112.61 0 0 0-8.16 5.92c-4.61 3.72-7.4 6.9-7.97 9.35-.63 2.67 1.48 4.53 7.05 5.46 10.7 1.78 20.92-.05 30.45-4.65a61.96 61.96 0 0 0 17.1-12.2 41.8 41.8 0 0 0 5.36-7.42v1.92a38.94 38.94 0 0 1-4.64 6.19 62.95 62.95 0 0 1-17.39 12.41c-9.7 4.68-20.13 6.55-31.05 4.73-6.06-1-8.65-3.29-7.85-6.67.64-2.74 3.53-6.05 8.31-9.9 2.35-1.9 5.1-3.88 8.24-5.97 2.73-1.82 5.58-3.61 8.9-5.62.72-.44 5.4-3.24 6.22-3.75 1.26-.8 2.6-1.76 4.3-3.09.8-.62 3.9-3.1 4.63-3.67 7.77-6.1 13.49-9.04 20.33-9.3zm0-154.6v1c-1.75-.24-4.3.23-7.82 1.55-10.01 3.75-13.8 5.07-19.15 6.76-1.78.56-2.63.83-3.87 1.24-1.48.5-3.16.76-6.74 1.16a1550.34 1550.34 0 0 0-2.64.3c-7.8.94-11.28 2.47-11.28 6.07 0 4.45 2.89 13.18 7.96 25.81a57.34 57.34 0 0 1 2.33 7.6 258.32 258.32 0 0 1 .84 3.46c1.86 7.62 3.17 10.71 5.56 11.67 2.21.88 4.7.6 7.47-.72 3.48-1.69 7.22-4.94 11.2-9.47 1.52-1.7 2.97-3.49 4.59-5.57l3.16-4.1c2.59-3.23 6.07-12.21 8.39-20.23v3.45c-2.29 7.2-5.27 14.5-7.61 17.41-.44.55-2.67 3.46-3.15 4.09-1.63 2.1-3.1 3.9-4.62 5.62-4.08 4.61-7.9 7.94-11.53 9.7-2.99 1.44-5.77 1.75-8.28.74-2.84-1.13-4.2-4.34-6.15-12.35a2097.48 2097.48 0 0 1-.84-3.46c-.8-3.2-1.47-5.45-2.28-7.46-5.14-12.8-8.04-21.55-8.04-26.19 0-4.37 3.84-6.06 12.16-7.07a160.9 160.9 0 0 1 2.65-.3c3.5-.39 5.15-.64 6.53-1.1 1.26-.42 2.1-.7 3.88-1.26 5.34-1.68 9.11-3 19.1-6.74 3.53-1.32 6.22-1.84 8.18-1.61zM0 292c10.13-11.31 18.13-23.2 23.07-35.39 3.3-8.14 6.09-16.12 10.81-30.55l1.59-4.84c6.53-19.94 10.11-29.82 14.77-39.56 6.07-12.72 12.55-21.18 20.27-25.54 6.66-3.76 10.2-7.86 12.22-13.15a46.6 46.6 0 0 0 1.86-6.58c1.23-5.2 2.05-7.59 3.93-10.36 2.45-3.62 6.27-6.53 12.1-8.96 15.78-6.58 16.73-7.04 18.05-9.01.65-.98.83-2.15.74-4.51-.03-.73-.23-3.82-.24-4A93.8 93.8 0 0 1 119 94c0-10.04.18-11.37 2.37-13.15.52-.42 1.13-.8 2.07-1.3.27-.14 2.18-1.12 2.84-1.48a68.4 68.4 0 0 0 9.12-5.87c2.06-1.54 2.64-2.14 8.01-7.93 3.78-4.09 6.21-6.36 8.96-8.12 3.64-2.33 7.2-3.12 10.9-2.11 4.4 1.2 10.81 2 18.78 2.46 6.9.4 12.9.5 21.95.5 4.87 0 8.97.47 15.4 1.57 7.77 1.33 9.3 1.54 12.38 1.54 4.05 0 7.43-.88 10.68-2.95 5.06-3.22 8.11-4.67 11.2-5.2 3.62-.64 4.77-.46 16.55 2.06 17.26 3.7 30.85 1.36 41.06-9.7 5.1-5.53 5.48-8.9 3.48-14.8-.83-2.42-1.03-3.1-1.17-4.3-.29-2.52.5-4.71 2.71-6.93 2.65-2.65 4.72-9.17 6.22-18.29h2.03c-1.56 9.71-3.77 16.65-6.83 19.7-1.79 1.8-2.36 3.39-2.14 5.28.11 1 .3 1.63 1.07 3.9 2.22 6.53 1.76 10.66-3.9 16.8-10.77 11.66-25.07 14.13-42.95 10.3-11.42-2.45-12.55-2.62-15.78-2.06-2.77.48-5.62 1.84-10.47 4.92a20.93 20.93 0 0 1-11.76 3.27c-3.25 0-4.81-.22-12.73-1.57C212.74 59.46 208.73 59 204 59c-9.1 0-15.11-.1-22.07-.5-8.09-.47-14.62-1.29-19.2-2.54-5.62-1.53-10.17 1.38-17.85 9.66-5.5 5.94-6.08 6.53-8.28 8.18a70.38 70.38 0 0 1-9.38 6.03c-.68.37-2.58 1.35-2.84 1.49-.84.44-1.35.76-1.75 1.08C121.16 83.6 121 84.8 121 94c0 1.85.06 3.54.17 5.44 0 .17.2 3.28.24 4.03.1 2.75-.13 4.29-1.08 5.71-1.67 2.5-2.27 2.8-18.95 9.74-5.48 2.29-8.99 4.96-11.2 8.24-1.71 2.51-2.47 4.73-3.64 9.7-.83 3.5-1.21 4.92-1.94 6.83-2.18 5.73-6.05 10.19-13.1 14.18-7.3 4.12-13.55 12.28-19.46 24.66-4.6 9.64-8.17 19.46-14.67 39.32l-1.58 4.84c-4.75 14.47-7.54 22.48-10.86 30.69-5.28 13.01-13.95 25.65-24.93 37.6v-2.97zm0 78v-.5l1-.01c6.32 0 7.47 5.2 4.6 13.36a60.36 60.36 0 0 1-5.6 11.3v-1.92a57.76 57.76 0 0 0 4.65-9.72c2.69-7.6 1.71-12.02-3.65-12.02-.34 0-.67 0-1 .02v-46.59a340.96 340.96 0 0 0 13.71-8.34c13.66-9.46 29.79-37.6 29.79-53.59 0-18.1 21.57-72.64 32.23-79.42 12.71-8.09 32.24-27.96 35.8-37.75 1.93-5.3 5.5-7.27 14.42-9.37 6.15-1.44 8.64-2.42 10.67-4.79 1.5-1.74 2.72-4.79 4.33-10.3.23-.78 1.9-6.68 2.43-8.46 3.62-12.08 7.3-18.49 13.47-20.39 2.5-.76 3.03-.98 9.74-3.7 7.49-3.03 11.97-4.43 17.12-4.92 6.75-.65 13.13.75 19.55 4.67 5.43 3.32 12.19 4.72 20.17 4.56 6.03-.12 12.2-1.07 19.83-2.8 1.82-.4 7.38-1.74 8.26-1.94 2.69-.6 4.34-.89 5.48-.89 4.97 0 8.93-.05 14.2-.27 7.9-.32 15.56-.92 22.75-1.88 8.5-1.14 15.9-2.73 21.88-4.82 18.9-6.62 32.64-18.3 33.67-27.59.29-2.56.4-2.96 2.79-11.11 2.33-7.95 3.21-12.93 2.72-18.23-.2-2.24-.69-4.38-1.48-6.42-1.5-3.92-2.63-9.4-3.43-16.18h.9c.77 6.47 1.89 11.72 3.47 15.82a24.93 24.93 0 0 1 1.54 6.69c.5 5.46-.4 10.54-2.77 18.6-2.36 8.06-2.47 8.47-2.74 10.95-1.09 9.75-15.1 21.68-34.33 28.41-6.06 2.12-13.52 3.72-22.09 4.87-7.22.96-14.92 1.57-22.83 1.89-5.3.21-9.27.27-14.25.27-1.04 0-2.64.27-5.26.87-.87.2-6.43 1.53-8.26 1.94-7.68 1.73-13.92 2.7-20.03 2.82-8.15.17-15.1-1.27-20.71-4.7-6.23-3.81-12.4-5.16-18.93-4.54-5.04.48-9.44 1.86-16.84 4.86-6.75 2.74-7.29 2.95-9.82 3.73-5.73 1.76-9.28 7.96-12.81 19.72-.53 1.77-2.2 7.66-2.43 8.46-1.66 5.65-2.91 8.78-4.53 10.67-2.22 2.58-4.84 3.62-12.01 5.3-7.8 1.83-11.13 3.66-12.9 8.54-3.65 10.04-23.32 30.06-36.2 38.25C65.94 190 44.5 244.2 44.5 262c0 16.34-16.3 44.78-30.22 54.41-2.14 1.48-8.24 5.12-14.28 8.68v-1.16 46.09zm0-173.7v-1.11c7.42-3.82 14.55-10.23 21.84-18.98 3.8-4.56 14.21-18.78 15.79-20.55 1.8-2.04 4.06-3.96 7.42-6.45 1.08-.8 4.92-3.57 5.49-3.99 9.36-6.85 14-11.96 15.98-19.36.8-2.98 1.54-6.78 2.46-12.3.23-1.44 2-12.46 2.56-15.79 2.87-16.77 5.73-26.79 10.07-32.1C92.46 52.43 101.5 38.13 101.5 33c0-2.54.34-3.35 6.05-15.71.68-1.49 1.25-2.74 1.77-3.93 2.5-5.75 3.9-10.04 4.14-13.36h1c-.23 3.48-1.66 7.87-4.23 13.76-.52 1.2-1.09 2.45-1.78 3.95-5.54 12.01-5.95 12.99-5.95 15.29 0 5.47-9.09 19.84-20.11 33.31-4.2 5.12-7.03 15.06-9.86 31.64-.57 3.33-2.33 14.33-2.57 15.78-.92 5.56-1.67 9.38-2.48 12.4-2.05 7.68-6.82 12.93-16.35 19.91l-5.49 3.98c-3.3 2.45-5.51 4.34-7.27 6.31-1.53 1.73-11.94 15.93-15.76 20.53-7.52 9.02-14.88 15.6-22.61 19.46zm0 361.83v-4.33c.48 2.36 1 4.35 1.6 6.15 2 6.03 4.6 8.26 8.19 6.59C28.76 557.69 43.5 542.4 43.5 527c0-16.2 6.37-31.99 17.1-46.3 1.88-2.5 3.66-4.4 5.53-6 .73-.62 1.45-1.18 2.3-1.8l2-1.43c3.68-2.68 5.32-5.28 7.08-12.59.75-3.07 1.38-5.02 4.2-13.26l.63-1.88c3.24-9.58 4.56-14.97 4.17-18.65-.48-4.43-3.8-5.23-11.3-1.64a81.12 81.12 0 0 1-9.15 3.7c-13.89 4.67-26.96 5.8-42.66 5.42l-1.95-.05-1.45-.02a39.8 39.8 0 0 0-15.05 2.96A21.81 21.81 0 0 0 0 438.37v-1.26a23.55 23.55 0 0 1 4.55-2.57 40.77 40.77 0 0 1 16.92-3.02l1.95.05c15.6.38 28.57-.75 42.32-5.37a80.12 80.12 0 0 0 9.04-3.65c8.04-3.84 12.16-2.85 12.72 2.43.42 3.89-.92 9.34-4.21 19.08l-.64 1.88c-2.8 8.2-3.43 10.15-4.16 13.18-1.82 7.52-3.59 10.34-7.47 13.16l-2 1.43c-.84.6-1.54 1.15-2.25 1.75a35.45 35.45 0 0 0-5.37 5.84c-10.61 14.15-16.9 29.74-16.9 45.7 0 15.88-15 31.45-34.29 40.45-4.3 2.01-7.39-.66-9.56-7.18-.23-.68-.44-1.39-.65-2.13zm0-62.16v-2.45l1.46 3.27c2.1 4.8 3.46 10.33 4.26 16.77.66 5.3.84 9.3 1.04 18.5.2 9.32.5 12.75 1.63 15.05 1.28 2.6 3.67 2.35 8.29-1.5 17.14-14.3 21.82-22.9 21.82-38.62 0-7.17 1.1-12.39 3.7-17.68 2.27-4.67 3.65-6.62 13.4-19.62a69.8 69.8 0 0 1 7.6-8.79 44.76 44.76 0 0 1 3.54-3.06c.38-.3.64-.52.89-.74a10.47 10.47 0 0 0 2.63-3.32 35.78 35.78 0 0 0 2.26-5.94l.37-1.2.36-1.15c.29-.91.48-1.55.66-2.16.45-1.53.74-2.68.91-3.66.38-2.2.12-3.49-.85-4.15-2.35-1.61-9.28-.24-23.8 4.94-9.54 3.4-16.12 4.17-27.85 4.26-7.71.06-10.43.4-13.25 2.12-3.48 2.12-5.84 6.4-7.58 14.26-.5 2.2-.99 4.19-1.49 5.98v-3.98l.51-2.22c1.8-8.1 4.28-12.6 8.04-14.9 3.04-1.85 5.86-2.2 13.77-2.26 11.61-.09 18.1-.84 27.51-4.2 14.93-5.32 21.95-6.71 24.7-4.83 1.38.94 1.71 2.6 1.28 5.15a33.69 33.69 0 0 1-.94 3.78l-.66 2.17-.36 1.15-.37 1.2a36.64 36.64 0 0 1-2.33 6.1c-.8 1.53-1.61 2.52-2.86 3.61l-.92.77-1.02.83c-.9.74-1.65 1.4-2.47 2.18a68.84 68.84 0 0 0-7.48 8.66c-9.7 12.93-11.07 14.87-13.31 19.46-2.52 5.15-3.59 10.22-3.59 17.24 0 16.04-4.82 24.91-22.18 39.38-5.04 4.2-8.18 4.55-9.83 1.18-1.22-2.5-1.52-5.94-1.73-15.47-.2-9.16-.38-13.15-1.03-18.4-.79-6.34-2.12-11.8-4.19-16.49L0 495.98zM379.27 0h1.04l1.5 5.26c3.28 11.56 4.89 19.33 5.26 27.8.49 11.01-1.52 21.26-6.63 31.17-7.8 15.13-20.47 26.5-36.22 34.1-12.38 5.96-26.12 9.17-36.22 9.17-6.84 0-17.24 1.38-37.27 4.62l-2.27.37c-24.5 3.99-31.65 5-37.46 5-3.49 0-4.08-.08-19.54-2.8-3.56-.64-6.32-1.1-9-1.5-20.23-2.96-31-1.2-31.96 7.86-.1.85-.18 1.72-.29 2.81l-.27 2.73c-1.1 10.9-2.02 15.73-4.31 19.96-2.9 5.34-7.77 7.95-15.63 7.95-10.2 0-12.92.6-15.5 3.17.52-.51-5.03 5.85-8.16 8.7-2.75 2.5-14.32 12.55-15.77 13.83a341.27 341.27 0 0 0-6.54 5.92c-6.97 6.49-11.81 11.76-14.6 16.15-5.92 9.3-10.48 18.04-11.69 24.08-1.66 8.3 3.67 9.54 19.02 1.21a626.23 626.23 0 0 1 44.54-21.9c3.5-1.56 14.04-6.2 15.68-6.95 5.05-2.25 8.3-3.8 10.78-5.15l1.95-1.07 2.18-1.18c1.76-.94 3.38-1.76 5-2.55 18.1-8.72 34.48-10.46 50.33-1.2 22.89 13.34 38.28 37.02 38.28 56.44 0 19.12-.73 25.13-5.18 33.2a45.32 45.32 0 0 1-4.94 7.12c-6.47 7.77-11.81 16.2-12.76 21.27-1.2 6.34 4.69 7.03 20.17-.05 13.31-6.08 22.4-14.95 28.5-26.32a80.51 80.51 0 0 0 6.1-15.13c.9-2.98 3.17-11.65 3.41-12.48a29.02 29.02 0 0 1 1.75-4.83c7.47-14.93 21.09-30.5 36.25-37.24 7.61-3.38 13-9.65 19.4-20.79.84-1.48 4.26-7.64 5.14-9.17 3.52-6.1 6.22-9.7 9.37-11.98 10.15-7.4 28.7-11.1 50.29-11.1 7.52 0 16.54-1.24 27.51-3.58a420.1 420.1 0 0 0 14.96-3.52c-1.3.33 15.54-3.98 19.42-4.89 14.15-3.33 41.07-5.01 64.11-5.01 17.36 0 27.82-9.23 38.53-38.67 6.62-18.21 6.62-26.37 2.69-34.35l-1.18-2.37A13.36 13.36 0 0 1 587.5 58c0-4.03 0-4.01 2.5-24.56.46-3.73.8-6.74 1.12-9.64.9-8.45 1.38-15.2 1.38-20.8 0-.94-.02-1.94-.04-3h1c.03 1.06.04 2.06.04 3 0 5.65-.48 12.43-1.39 20.9-.3 2.91-.66 5.93-1.11 9.66-2.5 20.45-2.5 20.47-2.5 24.44 0 1.97.45 3.57 1.45 5.68.24.51 1.16 2.35 1.17 2.36 4.06 8.24 4.06 16.68-2.65 35.13-10.84 29.8-21.63 39.33-39.47 39.33-22.96 0-49.83 1.68-63.89 4.99-3.86.9-20.69 5.2-19.4 4.88a421.05 421.05 0 0 1-14.99 3.53c-11.04 2.35-20.11 3.6-27.72 3.6-21.4 0-39.76 3.67-49.7 10.9-3 2.19-5.64 5.7-9.1 11.68-.87 1.52-4.29 7.68-5.14 9.17-6.49 11.3-12 17.71-19.86 21.2-14.9 6.63-28.38 22.03-35.75 36.77a28.17 28.17 0 0 0-1.69 4.67c-.23.8-2.5 9.49-3.4 12.5a81.48 81.48 0 0 1-6.19 15.3c-6.2 11.56-15.44 20.58-28.96 26.76-16.1 7.36-23 6.55-21.58-1.04 1-5.29 6.4-13.83 12.99-21.73a44.33 44.33 0 0 0 4.82-6.96c4.35-7.88 5.06-13.77 5.06-32.72 0-19.04-15.19-42.4-37.72-55.55-15.57-9.08-31.62-7.38-49.45 1.21a132.9 132.9 0 0 0-7.14 3.71l-1.95 1.07a158.83 158.83 0 0 1-10.85 5.19c-1.65.74-12.18 5.38-15.69 6.95a625.25 625.25 0 0 0-44.46 21.86c-15.95 8.66-22.37 7.16-20.48-2.29 1.24-6.2 5.83-15.02 11.82-24.42 2.85-4.48 7.74-9.8 14.77-16.34 1.98-1.85 4.12-3.79 6.56-5.94 1.46-1.29 13.02-11.33 15.75-13.82 3.09-2.8 8.6-9.14 8.14-8.67 2.82-2.82 5.75-3.46 16.2-3.46 7.5 0 12.04-2.43 14.75-7.42 2.2-4.07 3.11-8.84 4.2-19.59l.26-2.73.3-2.81c.56-5.42 4.47-8.5 11.23-9.6 5.44-.88 12.51-.51 21.86.86 2.7.4 5.47.86 9.04 1.49 15.33 2.7 15.96 2.8 19.36 2.8 5.73 0 12.9-1.03 37.3-5l2.27-.36c20.1-3.26 30.52-4.64 37.43-4.64 9.95 0 23.54-3.18 35.78-9.08 15.57-7.5 28.09-18.73 35.78-33.65 5.02-9.75 7-19.82 6.51-30.67-.37-8.37-1.96-16.08-5.23-27.57L379.27 0zm13.68 0h1.02c.78 3.9 1.92 8.7 3.51 14.88 3.63 14.05 3.06 27.03-.75 38.77a61 61 0 0 1-11.35 20.68 138.36 138.36 0 0 1-19.32 18.77c-11.32 9.02-23.36 15.49-35.95 18.39a258.63 258.63 0 0 1-22.57 4.07c-3.17.44-6.36.85-10.3 1.32l-9.39 1.12c-11.53 1.41-17.45 2.55-21.64 4.46-9.28 4.21-28.35 6.04-49.21 6.04-1.37 0-2.8-.12-4.3-.35-2.62-.41-5-1.03-9.14-2.29-7.34-2.21-9.63-2.75-12.63-2.56-3.9.23-6.63 2.29-8.47 6.89-1.86 4.66-2.42 7.53-3.34 14.98-1.1 8.98-2.87 12.12-9.97 14.3a40.12 40.12 0 0 0-6.8 2.66c-.63.33-1.16.64-1.76 1.02l-1.34.86c-1.9 1.14-3.86 1.49-9.25 1.49-3.2 0-8.83-.55-9.51-.39-1.22.28-.75-.14-7.14 6.24-1.5 1.5-3.49 3.18-6.32 5.37-1.52 1.18-7.16 5.43-7.94 6.03-4.96 3.78-8.33 6.6-11.06 9.38-4.88 4.98-6.85 9.15-5.56 12.7 1.34 3.67 4.07 4.42 8.9 2.82a55.72 55.72 0 0 0 7.77-3.48c1.5-.77 7.78-4.13 9.37-4.96a116.8 116.8 0 0 1 12.31-5.68 162.2 162.2 0 0 0 11.04-4.84c2.04-.97 10.74-5.16 13-6.22 4.41-2.1 8.1-3.78 11.65-5.29 17.14-7.3 29.32-9.9 37.67-6.65l5.43 2.1c2.3.88 4.17 1.62 6.02 2.38a150.9 150.9 0 0 1 13.07 6c18.34 9.63 30.35 22.13 34.79 39.87 6.96 27.85 3.6 45.53-8.08 62.4-3.97 5.75-3.52 9.2.06 8.97 4.14-.28 10.21-4.95 15.11-12.52 3.1-4.8 5.1-10.45 8.05-21.53l1.69-6.35c.66-2.47 1.24-4.52 1.83-6.5 4.93-16.56 11-27.28 21.56-34.76 7.15-5.06 23.73-15.5 25.48-16.75 6.74-4.81 10.53-9.44 14.34-18 7.74-17.44 21.09-24.34 44.47-24.34 9.36 0 17.91-1.13 29.53-3.49a624.86 624.86 0 0 0 6.2-1.28c2.4-.5 4.07-.84 5.66-1.13 4.03-.74 7.04-1.1 9.61-1.1 4.44 0 9.39-1 31.39-5.99l2.95-.66c16.34-3.67 25.64-5.35 31.66-5.35 1.54 0 2.4.01 6.4.1 7.8.15 12.27.13 17.33-.2 16.41-1.06 26.73-5.36 29.8-14.56a87.1 87.1 0 0 1 3.55-8.83c-.15.31 2.29-4.96 2.9-6.38 5.38-12.3 5.57-21.92-1.44-39.44a86.4 86.4 0 0 1-5.26-20.72c-1.61-11.98-1.38-23.14.1-40.35l.2-2.12h1l-.2 2.2c-1.48 17.15-1.7 28.24-.11 40.14a85.4 85.4 0 0 0 5.2 20.47c7.1 17.78 6.91 27.67 1.43 40.22-.62 1.43-3.06 6.72-2.91 6.4a86.17 86.17 0 0 0-3.52 8.73c-3.23 9.72-13.9 14.15-30.68 15.24-5.1.33-9.58.35-17.42.2-3.98-.09-4.84-.1-6.37-.1-5.91 0-15.18 1.67-31.44 5.32l-2.95.67c-22.16 5.02-27.05 6.01-31.61 6.01-2.5 0-5.45.36-9.43 1.09-1.58.29-3.25.62-5.64 1.11a4894.21 4894.21 0 0 0-6.2 1.29c-11.68 2.37-20.3 3.51-29.73 3.51-23.02 0-36 6.71-43.53 23.66-3.9 8.8-7.82 13.58-14.7 18.5-1.78 1.27-18.36 11.7-25.48 16.75-10.34 7.32-16.3 17.87-21.19 34.23-.58 1.96-1.15 4-1.82 6.47l-1.69 6.35c-2.98 11.18-5 16.9-8.17 21.81-5.05 7.81-11.37 12.68-15.89 12.98-4.7.31-5.3-4.23-.94-10.53 11.52-16.64 14.82-34.03 7.92-61.6-4.35-17.42-16.16-29.72-34.27-39.22-4-2.1-8.2-4-12.99-5.97-1.84-.75-3.7-1.49-6-2.38l-5.43-2.08c-8.03-3.12-20.02-.58-36.92 6.63-3.52 1.5-7.21 3.19-11.61 5.27l-13 6.22c-4.71 2.22-8.16 3.75-11.11 4.88a115.87 115.87 0 0 0-12.21 5.63c-1.58.83-7.86 4.18-9.37 4.96a56.55 56.55 0 0 1-7.9 3.54c-5.3 1.75-8.62.85-10.17-3.43-1.46-4.02.66-8.5 5.8-13.74 2.75-2.82 6.16-5.66 11.15-9.48.79-.6 6.43-4.85 7.94-6.02a66.96 66.96 0 0 0 6.23-5.28c6.74-6.74 6.1-6.16 7.61-6.51.87-.2 6.69.36 9.74.36 5.22 0 7.03-.32 8.74-1.35l1.31-.84c.62-.4 1.18-.72 1.84-1.07a41.07 41.07 0 0 1 6.96-2.72c6.64-2.04 8.22-4.84 9.28-13.47.93-7.53 1.5-10.47 3.4-15.24 1.99-4.95 5.04-7.26 9.34-7.51 3.17-.2 5.5.35 12.97 2.6a63.54 63.54 0 0 0 9.02 2.26c1.45.22 2.83.34 4.14.34 20.71 0 39.7-1.82 48.8-5.96 4.32-1.96 10.29-3.1 21.93-4.53l9.4-1.12c3.92-.48 7.11-.88 10.27-1.32 8.16-1.14 15.4-2.43 22.49-4.06 12.42-2.86 24.33-9.26 35.55-18.2a137.4 137.4 0 0 0 19.18-18.64 60.02 60.02 0 0 0 11.15-20.32c3.76-11.57 4.32-24.36.75-38.23A284.86 284.86 0 0 1 392.95 0zM506.7 0h1.26c-.5.66-.9 1.18-1.17 1.51-3.95 4.96-6.9 7.92-9.82 9.57A10.02 10.02 0 0 1 492 12.5c-2.38 0-4.24.67-6.71 2.21l-2.65 1.71c-4.38 2.8-8.01 4.08-13.64 4.08-5.6 0-9.99-1.26-16.08-4.05a202.63 202.63 0 0 1-2.3-1.06l-2.18-.98c-1.6-.7-2.92-1.17-4.17-1.48a13.42 13.42 0 0 0-3.27-.43c-2.3 0-4.3-.68-11-3.37l-1.56-.62c-5-1.97-8.1-2.82-10.52-2.66-2.93.2-4.42 2.03-4.42 6.15 0 20.76-5.21 50.42-12.15 57.35-7.58 7.59-26.55 23.7-34.06 29.06-13.16 9.4-31.17 20.2-44.11 25.06a106.87 106.87 0 0 1-13.32 4.03c-3.28.78-6.6 1.43-11.25 2.24-.53.1-8.8 1.5-11.5 1.99-4.86.87-9.3 1.74-14 2.76-20.62 4.48-25.07 5.01-38.11 5.01-2.49 0-2.9-.07-14.05-2-2.42-.42-4.31-.73-6.15-1-8.11-1.19-13.83-1.36-17.64-.2-4.54 1.4-5.93 4.65-3.7 10.52 2.02 5.28 4.84 8.61 8.84 10.74 3.26 1.74 6.75 2.6 13.82 3.71 9.42 1.48 10.94 1.75 15.5 2.92a78.2 78.2 0 0 1 18.62 7.37c8.3 4.58 14.58 11.5 19.98 20.89 2.73 4.73 9.46 19.33 10.54 21.19 3.4 5.85 6.26 6.63 10.89 2 4.95-4.94 10.35-8.37 21.13-14.06.47-.25 2.06-1.1 2.12-1.12 7.98-4.21 11.92-6.51 15.87-9.54 5.11-3.9 8.66-8.1 10.77-13.11 8.52-20.24 20.75-33.31 32.46-33.31l5.5.03c10.53.08 17.35.02 24.9-.31 13.66-.62 23.78-2.09 29.39-4.67 5.85-2.7 13.42-5.49 24.18-9.02 3.46-1.14 6.29-2.05 12.7-4.1 7.7-2.45 11.08-3.54 15.17-4.9a1059.43 1059.43 0 0 1 11.33-3.72c3.67-1.2 5.96-2 8.03-2.78a59.88 59.88 0 0 0 6.66-2.94c1.87-.98 3.76-2.1 5.86-3.5 3.48-2.33 6.15-3.13 12.04-4.13l1.15-.2c5.71-1.01 9-2.3 12.76-5.63 7.82-6.96 8.58-23.18 3.84-44.52-1.7-7.67-2.1-19.28-1.57-35.47A837.22 837.22 0 0 1 546.76 0h1l-.15 3.06c-.32 6.42-.53 11.02-.68 15.62-.51 16.1-.12 27.65 1.56 35.21 4.82 21.68 4.04 38.2-4.16 45.48-3.91 3.48-7.37 4.84-13.24 5.87l-1.16.2c-5.76.99-8.32 1.75-11.65 3.98a63.73 63.73 0 0 1-5.96 3.56 60.86 60.86 0 0 1-6.77 2.99c-2.09.79-4.39 1.58-8.07 2.79a5398.31 5398.31 0 0 1-11.32 3.71c-4.1 1.37-7.48 2.46-15.18 4.92-6.42 2.04-9.24 2.95-12.7 4.08-10.73 3.53-18.27 6.3-24.07 8.98-5.76 2.66-15.97 4.14-29.77 4.77-7.56.33-14.4.39-24.95.31l-5.49-.03c-11.19 0-23.16 12.79-31.54 32.7-2.19 5.19-5.84 9.52-11.08 13.52-4.02 3.07-7.99 5.39-16.01 9.62l-2.12 1.12c-10.7 5.65-16.04 9.04-20.9 13.9-5.14 5.14-8.75 4.15-12.45-2.22-1.12-1.92-7.85-16.5-10.54-21.2-5.33-9.24-11.48-16.02-19.6-20.5a77.2 77.2 0 0 0-18.4-7.28c-4.5-1.17-6.02-1.43-15.4-2.9-7.17-1.12-10.74-2-14.13-3.81-4.22-2.25-7.2-5.77-9.3-11.27-2.43-6.39-.78-10.26 4.34-11.83 4-1.22 9.82-1.05 18.08.17 1.84.27 3.74.58 6.17 1 11.02 1.9 11.48 1.98 13.88 1.98 12.96 0 17.35-.52 37.9-4.99 4.71-1.02 9.16-1.9 14.03-2.77 2.71-.48 10.98-1.9 11.5-1.98 4.64-.81 7.95-1.46 11.2-2.23 4.55-1.07 8.76-2.34 13.2-4 12.83-4.81 30.79-15.59 43.88-24.94 7.47-5.33 26.4-21.4 33.94-28.94C407.3 61.98 412.5 32.49 412.5 12c0-4.61 1.86-6.9 5.35-7.15 2.63-.18 5.8.7 10.96 2.73l1.56.62c6.53 2.62 8.53 3.3 10.63 3.3 1.14 0 2.3.16 3.5.46 1.32.33 2.68.82 4.34 1.53a90.97 90.97 0 0 1 3.34 1.52l1.15.54c5.98 2.73 10.23 3.95 15.67 3.95 5.41 0 8.87-1.21 13.1-3.92.2-.13 2.1-1.38 2.66-1.72 2.62-1.63 4.64-2.36 7.24-2.36 1.47 0 2.94-.43 4.47-1.3 2.78-1.56 5.67-4.45 9.54-9.31l.7-.89zM324.54 600h-2.03c.49-2.96.91-6.2 1.28-9.66.44-4.1.76-8.25.98-12.21.08-1.39.14-2.65-.35-7.29-.47-1.94-.93-4.14-1.36-6.54-2.01-11.26-2.66-22.9-1.14-33.78a60.76 60.76 0 0 1 5.18-17.95 70.78 70.78 0 0 1 12.6-18.22c3.38-3.6 5.53-5.5 11.83-10.79 4.5-3.78 6.35-5.56 7.52-7.5.64-1.07.95-2.06.95-3.06 0-1.75 0-1.74-.75-9.23-.36-3.7-.57-6.3-.68-8.96-.5-12.1 1.62-19.6 8.11-21.76 15.9-5.3 25.89-12.1 33.45-25.54C409.6 390.65 425.85 376 436 376c12.36 0 20-1.96 29.41-8.8 6.76-4.92 9.5-6.6 12.47-7.46 2.22-.64 3.8-.74 9.12-.74 1.86 0 3.53-.83 5.57-2.62 1.08-.96 5.11-5.12 5.6-5.6 6.04-5.85 11.98-8.78 20.83-8.78 2.45 0 4.54.04 7.32.12 7.51.23 8.87.17 11.27-.7 3.03-1.1 5.53-3.03 14.75-11.17 8-7.06 10.72-8.92 22.87-16.47 1.44-.9 2.59-1.63 3.69-2.37a69.45 69.45 0 0 0 9.46-7.5c4.12-3.88 8.02-7.85 11.64-11.9v2.98a201.58 201.58 0 0 1-10.27 10.38c-3.18 3-6.2 5.35-9.72 7.7-1.12.76-2.28 1.5-3.75 2.4-12.05 7.5-14.71 9.32-22.6 16.28-9.46 8.35-12.01 10.32-15.39 11.55-2.74 1-4.19 1.06-12.01.82-2.76-.08-4.83-.12-7.26-.12-8.27 0-13.75 2.7-19.43 8.22-.44.43-4.52 4.64-5.68 5.66-2.37 2.09-4.46 3.12-6.89 3.12-5.1 0-6.6.1-8.56.66-2.67.78-5.29 2.37-11.85 7.15-9.8 7.13-17.85 9.19-30.59 9.19-9.22 0-24.96 14.2-34.13 30.49-7.84 13.94-18.24 21.02-34.55 26.46-5.31 1.77-7.21 8.51-6.75 19.78.1 2.6.31 5.19.68 8.84.75 7.62.75 7.58.75 9.43 0 1.38-.42 2.73-1.24 4.09-1.33 2.2-3.26 4.07-7.94 8-6.25 5.24-8.36 7.12-11.67 10.63a68.8 68.8 0 0 0-12.25 17.71 58.8 58.8 0 0 0-5 17.36c-1.49 10.66-.85 22.09 1.13 33.15.43 2.37.88 4.53 1.33 6.44.16.66.3 1.25.6 4.06a249.3 249.3 0 0 1-1.17 16.12c-.37 3.37-.78 6.53-1.25 9.44zm-13.4 0h-1.05l.12-.28c3.07-7.16 4.29-11.83 4.29-18.72 0-3.57-.07-4.93-.76-15.65-.77-12.04-1-19.64-.55-28.3.58-11.5 2.4-22.1 5.81-32.16 1.3-3.8 2.8-7.5 4.55-11.1 3.46-7.14 6.83-12.39 10.42-16.6a59.02 59.02 0 0 1 4.35-4.56c.43-.4 3-2.8 3.67-3.45 5.72-5.6 7.51-11.52 7.51-29.18 0-18.84 2.9-23.77 15.82-28.24 1.09-.37 1.92-.67 2.77-.98a51.3 51.3 0 0 0 6.1-2.7c4.95-2.6 9.64-6.22 14.44-11.42 25.5-27.63 37.15-35.16 56.37-35.16 8.28 0 14.54-1.95 22-6.3 1.78-1.03 13.82-8.82 18.16-11.27 2.83-1.59 5.66-3.03 8.63-4.39 7.92-3.6 13.97-4.45 26.6-4.8 7.53-.2 10.7-.49 14.26-1.58 4.55-1.4 8.06-4 10.93-8.43 2.2-3.41 6.85-7.08 14.66-12.06 1.61-1.03 3.27-2.05 5.65-3.5 9.53-5.85 11.56-7.13 14.81-9.57 5.34-4 9.3-8.37 13.68-14.77a204.2 204.2 0 0 0 5.62-8.75v1.9c-1.97 3.17-3.4 5.38-4.8 7.42-4.42 6.48-8.46 10.92-13.9 15-3.29 2.46-5.32 3.75-14.89 9.61a375.06 375.06 0 0 0-5.63 3.5c-7.7 4.9-12.26 8.52-14.36 11.76-3 4.63-6.7 7.39-11.48 8.85-3.68 1.12-6.9 1.42-14.53 1.63-12.5.34-18.44 1.18-26.2 4.7a111.08 111.08 0 0 0-8.56 4.35c-4.3 2.43-16.34 10.22-18.15 11.27-7.6 4.43-14.03 6.43-22.5 6.43-18.87 0-30.3 7.4-55.63 34.84-4.88 5.28-9.67 8.97-14.7 11.62-2 1.05-4 1.92-6.23 2.75-.86.32-1.7.62-5.37 1.87-5.08 1.76-7.44 3.25-9.28 6.37-2.23 3.78-3.29 9.94-3.29 20.05 0 17.9-1.87 24.07-7.8 29.89-.69.67-3.27 3.06-3.69 3.46a58.04 58.04 0 0 0-4.28 4.49c-3.53 4.14-6.86 9.32-10.28 16.38a95.19 95.19 0 0 0-4.5 10.99c-3.38 9.97-5.18 20.48-5.76 31.9-.44 8.6-.22 16.17.55 28.17.69 10.76.76 12.12.76 15.72 0 6.35-1.02 10.87-4.35 19zm25.08 0h-1c-.04-4.73.06-9.39.28-15.02.26-6.41-.4-11.79-2.53-24.37l-.31-1.86c-2.12-12.55-2.76-19.35-1.97-26.47 1.03-9.25 4.75-16.68 12-22.67 22.04-18.2 29.81-30.18 29.81-44.61 0-2.6-.3-4.81-.98-8.17-.97-4.79-1.1-5.68-.97-7.57.2-2.56 1.27-4.7 3.56-6.72 2.67-2.35 7.05-4.6 13.72-7.01 9.72-3.5 15.52-9.18 24.3-21.57l1.78-2.5c4.48-6.33 7.1-9.63 10.43-12.78 4.31-4.07 8.98-6.77 14.54-8.17 13.3-3.32 20.37-5.47 25.34-7.64a49.5 49.5 0 0 0 5.28-2.7c1.1-.65 1.75-1.04 4.24-2.6 2.7-1.68 5.22-2.08 11.38-2.28 5.44-.18 7.9-.43 10.97-1.41a21.47 21.47 0 0 0 9.54-6.22c4.87-5.3 10.03-7.61 17.79-8.9 1.07-.18 1.88-.3 3.86-.58 6.9-.97 9.94-1.69 13.48-3.62 4.5-2.45 6.79-4.44 23.46-19.68l3.14-2.85c9.65-8.71 16.12-13.83 21.42-16.48 4.25-2.12 7.6-4.69 11.22-8.6v1.45c-3.42 3.57-6.69 6-10.78 8.05-5.18 2.59-11.61 7.67-21.2 16.32l-3.12 2.85c-16.8 15.35-19.05 17.3-23.66 19.82-3.68 2-6.8 2.75-13.82 3.73-1.97.28-2.78.4-3.84.57-7.56 1.26-12.52 3.48-17.21 8.6a22.47 22.47 0 0 1-9.97 6.5c-3.2 1-5.72 1.27-11.25 1.45-5.98.2-8.39.57-10.89 2.13a144 144 0 0 1-4.25 2.61 50.48 50.48 0 0 1-5.39 2.75c-5.04 2.2-12.15 4.37-25.5 7.7-9.74 2.44-15.26 7.65-24.4 20.56l-1.77 2.5c-8.9 12.54-14.82 18.34-24.78 21.93-6.57 2.36-10.85 4.57-13.4 6.82-2.1 1.86-3.05 3.74-3.22 6.04-.13 1.76 0 2.63.95 7.3.7 3.42 1 5.7 1 8.37 0 14.79-7.93 27-30.18 45.39-7.03 5.8-10.64 13-11.64 22-.78 7-.14 13.73 1.96 26.2l.32 1.85c2.15 12.65 2.8 18.07 2.54 24.58-.22 5.57-.32 10.2-.28 14.98zM95.9 600h-2.04c.68-3.82 1.14-8.8 1.61-15.98.2-3.11.27-4.06.39-5.6 1.3-17.54 4.04-27.14 11.5-33.2 4.65-3.77 7.22-8.92 8.67-16 .51-2.52.7-3.87 1.33-9.17.66-5.5 1.16-8.06 2.24-10.36 1.45-3.09 3.82-4.69 7.39-4.69 14.28 0 38.48 9.12 53.6 20.2 8.66 6.35 21.26 13.32 31.74 17.11 13.03 4.71 21.89 4.41 24.75-1.73 1.7-3.64 1.92-4.11 2.65-5.77 2.93-6.67 4.69-12.2 5.25-17.5.23-2.17.24-4.23.02-6.2-.32-2.75-1.42-4.55-4.08-7.35l-1.32-1.37a30.59 30.59 0 0 1-2.41-2.79 30.37 30.37 0 0 1-2.5-4.07l-1.13-2.14c-1.62-3.1-2.68-4.6-4.12-5.56-5.26-3.5-14.8-5.5-28.55-6.83a272.42 272.42 0 0 0-9.04-.71l-2.18-.17c-9.57-.73-15.12-1.56-19.06-3.2C156.57 471.07 136 450.5 136 440c0-5.34 1.74-9.53 5.47-14.13 1.98-2.44 11.12-11.71 12.79-13.54 4.52-4.97 10.16-9.54 17.68-14.66 2.8-1.9 14.78-9.6 17.49-11.49a50.54 50.54 0 0 0 6.34-5.43c1.53-1.5 6.96-7.13 7.12-7.3 7.18-7.3 12.7-11.56 19.74-14.38 3.36-1.34 8.13-2.79 17.45-5.38a9577.18 9577.18 0 0 1 11.78-3.28 602.6 602.6 0 0 0 12.67-3.7c20.4-6.24 34-12.08 40.79-18.44 8.74-8.2 11.78-13.84 15.73-26.02 2.02-6.22 3.09-9.04 5.07-12.72 9.54-17.71 28.71-39.37 43.5-45.45C383.77 238.25 389 232.34 389 226c0-2.89 2.73-8.4 6.83-13.73 4.76-6.2 10.65-11.36 16.75-14.18 12.5-5.77 33.5-10.09 47.42-10.09 5.32 0 9.83-1.5 16.42-4.89 9.2-4.71 10.1-5.11 13.58-5.11 10.42 0 32.06-2.55 45.76-5.97l3.88-.98 3.47-.89c2.6-.66 4.33-1.08 5.93-1.43 3.9-.86 6.76-1.23 9.58-1.17 2.74.06 5.47.52 8.67 1.48 4.56 1.37 13.71-.9 22.87-5.68a68.07 68.07 0 0 0 9.84-6.2v2.4c-11.09 8.14-25.76 13.66-33.29 11.4a29.72 29.72 0 0 0-8.13-1.4c-2.63-.05-5.36.3-9.11 1.12a238 238 0 0 0-9.33 2.3l-3.9.99C522.38 177.43 500.58 180 490 180c-2.99 0-3.91.4-12.67 4.89-6.85 3.51-11.61 5.11-17.33 5.11-13.65 0-34.35 4.26-46.58 9.9-5.78 2.67-11.42 7.62-16 13.58-3.85 5.02-6.42 10.2-6.42 12.52 0 7.27-5.8 13.82-20.62 19.92-14.27 5.88-33.16 27.21-42.5 44.55-1.9 3.55-2.95 6.28-4.93 12.4-4.05 12.47-7.23 18.39-16.27 26.86-7.08 6.64-20.87 12.57-41.57 18.89a604.52 604.52 0 0 1-12.7 3.71 1495.1 1495.1 0 0 1-11.8 3.28c-9.24 2.58-13.97 4.01-17.24 5.32-6.73 2.69-12.05 6.8-19.05 13.92-.15.15-5.6 5.8-7.15 7.32a52.4 52.4 0 0 1-6.6 5.65c-2.74 1.92-14.75 9.63-17.5 11.5-7.4 5.04-12.94 9.52-17.33 14.35-1.72 1.9-10.8 11.11-12.71 13.46-3.47 4.26-5.03 8.03-5.03 12.87 0 9.5 20 29.5 33.38 35.08 3.67 1.53 9.1 2.34 18.45 3.05a586.23 586.23 0 0 0 4.34.32c3.24.23 5.07.37 6.93.55 14.08 1.37 23.82 3.4 29.45 7.17 1.82 1.2 3.02 2.91 4.8 6.29l1.11 2.13a28.55 28.55 0 0 0 2.34 3.81c.62.83 1.3 1.6 2.26 2.61.23.24 1.1 1.16 1.32 1.37 2.93 3.09 4.24 5.23 4.61 8.5.24 2.12.23 4.33-.01 6.64-.59 5.55-2.4 11.25-5.41 18.1-.74 1.67-.96 2.15-2.66 5.8-3.49 7.47-13.33 7.8-27.25 2.77-10.67-3.86-23.43-10.92-32.25-17.38C164.62 515.96 140.82 507 127 507c-5 0-6.4 3.02-7.64 13.29a99.03 99.03 0 0 1-1.36 9.33c-1.53 7.5-4.3 13.04-9.37 17.16-6.87 5.58-9.5 14.78-10.77 31.8-.11 1.52-.18 2.47-.38 5.57-.46 7.01-.91 11.99-1.57 15.85zm8.05 0h-1.02c.29-1.41.58-2.94.9-4.59l1.05-5.62c2.5-13.3 4.2-19.92 6.68-24.05 1.7-2.84 3.68-5.5 8.05-11.03 8.21-10.36 10.88-14.55 10.88-18.71l-.02-1.69c-.02-1.78-.02-2.7.02-3.77.21-5.05 1.47-8.2 4.64-9.4 3.92-1.5 10.39.44 20.12 6.43 9.56 5.88 17.53 10.7 25.91 15.66 1.31.78 14.27 8.41 17.67 10.45a714.21 714.21 0 0 1 6.42 3.9c13.82 8.5 38.94 5.05 46.3-7.83 3.6-6.28 4.54-8.52 7.78-17.32a82.3 82.3 0 0 1 1.18-3.07 42.27 42.27 0 0 1 4.06-7.64c9.33-13.98 14.92-26.1 14.92-36.72 0-3.66.75-6.62 3.36-14.85.52-1.64.83-2.66 1.15-3.73 3.64-12.23 3.04-19.12-4.29-24a23.1 23.1 0 0 0-9.98-3.78c-7.2-.93-14.49 1.17-23.91 5.88-1.55.78-6.64 3.44-7.6 3.93a62.6 62.6 0 0 0-4.14 2.3l-4.4 2.66c-11.62 6.92-20.4 9.18-32.81 6.08-3.32-.84-6.24-1.4-13.1-2.64-13.25-2.39-18.7-3.75-23.33-6.46-6.23-3.67-7.46-9.02-2.88-16.65A93.1 93.1 0 0 1 172 415.42a157 157 0 0 1 8.32-7.66c-.07.05 6.16-5.3 7.82-6.77a85.12 85.12 0 0 0 6.5-6.33c7.7-8.46 12.78-13.36 20.08-18.57 9.94-7.1 21.4-12.36 35.18-15.58 37.03-8.64 51-12.7 58.83-17.93 8.6-5.73 21.3-24.77 36.84-54.81 5.22-10.1 12.27-18.4 21.13-25.71 5.13-4.24 9.56-7.25 17.55-12.23 7.42-4.62 9.62-6.14 11.38-8.16a21.15 21.15 0 0 0 2.95-4.87c.61-1.3 2.87-6.47 3-6.77 1.36-3 2.56-5.4 3.95-7.73 6.53-10.97 16.03-18 31.4-20.8 12.73-2.3 19.85-2.7 29.68-2.3 3.25.13 4.13.16 5.6.14 5.15-.07 9.71-1.04 16.61-3.8 20.74-8.3 38.75-12.04 59.19-12.04 3.05 0 6.03.15 10.48.48l2.09.16c12.45.96 18.08.96 25.34-.63a49.65 49.65 0 0 0 14.09-5.45v1.15a50.52 50.52 0 0 1-13.88 5.28c-7.38 1.61-13.08 1.61-25.63.65l-2.08-.16c-4.43-.33-7.39-.48-10.41-.48-20.3 0-38.2 3.72-58.81 11.96-7.01 2.8-11.7 3.8-16.97 3.88-1.5.02-2.39-.01-5.66-.14-9.76-.4-16.8-.01-29.47 2.3-15.06 2.73-24.32 9.58-30.71 20.31a72.8 72.8 0 0 0-3.9 7.63c-.12.28-2.39 5.47-3.01 6.79a22 22 0 0 1-3.1 5.1c-1.86 2.13-4.07 3.66-11.6 8.35-7.95 4.96-12.35 7.95-17.44 12.15-8.76 7.23-15.73 15.43-20.89 25.4-15.61 30.2-28.36 49.32-37.16 55.19-7.98 5.32-21.97 9.39-59.17 18.07-13.65 3.18-24.98 8.39-34.82 15.42-7.22 5.16-12.27 10.01-19.92 18.43a86.07 86.07 0 0 1-6.57 6.4c-1.67 1.48-7.91 6.83-7.84 6.77-3.27 2.84-5.8 5.16-8.26 7.62a92.1 92.1 0 0 0-14.27 18.13c-4.3 7.16-3.22 11.89 2.53 15.26 4.47 2.63 9.88 3.99 23.24 6.39a185.7 185.7 0 0 1 12.92 2.6c12.11 3.03 20.64.84 32.06-5.96l4.4-2.65c1.66-1 2.96-1.73 4.2-2.35.95-.48 6.04-3.14 7.6-3.92 9.59-4.8 17.04-6.94 24.49-5.98a24.1 24.1 0 0 1 10.4 3.93c7.82 5.21 8.45 12.52 4.7 25.13-.32 1.07-.64 2.1-1.16 3.74-2.57 8.12-3.31 11.04-3.31 14.55 0 10.88-5.66 23.14-15.08 37.28a41.28 41.28 0 0 0-3.97 7.46c-.37.9-.73 1.82-1.18 3.04-3.25 8.85-4.21 11.13-7.84 17.47-7.67 13.42-33.43 16.95-47.7 8.18a578.4 578.4 0 0 0-6.4-3.89c-3.4-2.04-16.36-9.67-17.67-10.45-8.38-4.97-16.36-9.78-25.92-15.66-9.5-5.85-15.7-7.7-19.24-6.36-2.68 1.02-3.8 3.82-4 8.51a61.12 61.12 0 0 0-.02 3.72l.02 1.7c0 4.5-2.69 8.73-11.52 19.87-3.92 4.95-5.87 7.59-7.55 10.39-2.39 3.97-4.08 10.56-6.56 23.72l-1.05 5.62-.86 4.4zm10.5 0h-1c.03-.34.04-.68.04-1 0-12.39 8.48-33.57 19.16-43.37a26.18 26.18 0 0 0 3.67-4.17 35.8 35.8 0 0 0 2.88-4.9c.36-.72 1.75-3.66 2.1-4.36 3.22-6.29 6.84-6.54 16.97.39 1.34.9 6.07 4.16 6.4 4.38 2.62 1.8 4.67 3.2 6.7 4.56 5.03 3.39 9.37 6.2 13.51 8.7 14.33 8.67 25.49 13.27 34.11 13.27 16.86 0 32.71-5.95 39.6-14.8 1.59-2.04 3.2-5.17 5.06-9.63.8-1.92 1.64-4.06 2.67-6.8l2.74-7.33c4.66-12.44 7.76-19.06 11.56-23.27 7.9-8.79 14.87-36 14.87-52.67 0-1.9.17-3.11 1.02-8.27.37-2.2.58-3.6.74-5.07.63-5.51.21-9.46-1.68-12.39-4.6-7.1-19.7-9.23-38.46-4.78a100.57 100.57 0 0 0-18.94 6.3c-5.17 2.37-17.11 9.74-16.5 9.4-6.72 3.64-12.97 4.15-24.8 1.3-29.55-7.14-30.43-8.62-15.26-26.81 17.44-20.93 47.12-46.18 56.38-46.18 9.92 0 53.84-11.98 65.78-17.95 9.46-4.73 24.32-21.18 36.82-37.85.71-.95 13.5-21.6 19.2-29.6 9.35-13.13 18.22-22.55 26.95-27.53 7.29-4.17 13.16-10.28 18.8-18.73 1.93-2.9 10.52-17.65 12.73-20.41 1.54-1.93 3-3.21 4.52-3.89 14.07-6.25 24.22-9.04 39.2-9.04h29c4.05 0 7.36-.4 22.93-2.5l4.3-.57c9.92-1.3 16.57-1.93 21.77-1.93 1.66 0 2.95.01 6.03.04 18.61.19 28.55-.48 44.86-4.03 3.1-.67 6.13-1.78 9.11-3.31v1.12a37.96 37.96 0 0 1-8.9 3.17c-16.4 3.56-26.4 4.24-45.08 4.05-3.08-.03-4.36-.04-6.02-.04-5.15 0-11.76.63-21.64 1.92l-4.3.58c-15.64 2.11-18.94 2.5-23.06 2.5h-29c-14.81 0-24.84 2.75-38.8 8.96-1.34.6-2.69 1.78-4.14 3.6-2.16 2.68-10.72 17.39-12.68 20.33-5.72 8.57-11.7 14.8-19.13 19.04-8.57 4.9-17.36 14.23-26.63 27.24-5.68 7.97-18.47 28.64-19.22 29.63-12.6 16.8-27.52 33.32-37.18 38.15-12.06 6.03-56.14 18.05-66.22 18.05-8.82 0-38.39 25.15-55.62 45.82-14.6 17.52-14.19 18.21 14.74 25.2 11.6 2.8 17.6 2.3 24.09-1.2-.67.35 11.31-7.03 16.56-9.44 5.41-2.48 11.6-4.59 19.11-6.37 19.13-4.53 34.65-2.35 39.54 5.22 2.05 3.17 2.48 7.32 1.84 13.04a96.34 96.34 0 0 1-.75 5.13c-.84 5.08-1.01 6.29-1.01 8.1 0 16.9-7.03 44.33-15.13 53.33-3.68 4.09-6.76 10.65-11.37 22.96-.35.93-2.2 5.94-2.73 7.33-1.04 2.76-1.88 4.9-2.68 6.84-1.9 4.53-3.55 7.73-5.2 9.85-7.1 9.13-23.25 15.19-40.39 15.19-8.86 0-20.15-4.65-34.63-13.42-4.15-2.51-8.5-5.32-13.55-8.72a861.54 861.54 0 0 1-6.71-4.56l-6.4-4.39c-9.68-6.63-12.61-6.42-15.5-.75-.35.68-1.74 3.62-2.1 4.35a36.77 36.77 0 0 1-2.96 5.03c-1.12 1.57-2.37 3-3.81 4.33-10.47 9.6-18.84 30.51-18.84 42.63l-.03 1zm-29.65 0h-1.1c1.17-2.52 1.79-5.2 1.79-8 0-20 4.83-42.04 12.15-49.35 5.17-5.18 7.77-8.38 9.9-12.74 2.64-5.41 3.95-12 3.95-20.91 0-6.82 1.14-11.59 3.37-15.07 1.74-2.7 3.6-4.21 8.91-7.52a31.64 31.64 0 0 0 3.9-2.79c4.61-3.96 6.58-6.2 7.72-9.41 1.43-4.02.93-9.04-1.86-16.02a68.98 68.98 0 0 0-3.99-8.07l-.93-1.7a75.47 75.47 0 0 1-2.64-5c-5.16-10.71-3.77-18.9 7.68-29.78a204 204 0 0 1 26.81-21.55c3.96-2.69 16.8-10.8 19.24-12.5 1.99-1.4 4.33-3.3 7.77-6.3-.02 0 7.23-6.39 9.47-8.3 4.97-4.26 9.09-7.5 13.05-10.15 4.72-3.15 8.97-5.28 12.87-6.32 12.78-3.41 15.6-4.18 21.77-5.97 12.55-3.64 21.96-6.9 28.14-10a45.47 45.47 0 0 1 7.47-2.79c8.66-2.66 12.02-4.1 16.97-8.1 6.78-5.46 13.07-14.25 19.33-27.87 15.97-34.77 19.08-39.39 32.15-49.19 3.14-2.36 6.37-4.1 11.43-6.4l2.33-1.04c11.93-5.35 16.87-8.93 21.1-17.38 1.88-3.77 2.48-6.29 3.37-12.27.78-5.19 1.48-7.56 3.53-10.25 2.57-3.4 7.03-6.27 14.36-9.01 3.37-1.26 7.36-2.5 12.05-3.73 16.33-4.3 25.28-5.36 39.6-5.81 6.9-.22 9.5-.56 12.66-2 1.19-.54 2.36-1.23 3.58-2.11 3.7-2.7 8.14-4.54 13.24-5.67 5.71-1.27 10.69-1.54 18.7-1.45l2.35.02c2.82 0 6.8-1 19.7-4.69 10.83-3.08 15.95-4.31 19.3-4.31.82 0 1.9.13 3.55.41l5.01.9c9.82 1.68 17.44 1.89 25.15-.21 7.98-2.18 14.8-6.77 20.29-14.24V147c-5.47 7.04-12.21 11.42-20.03 13.55-7.88 2.15-15.63 1.94-25.58.23l-5-.9c-1.6-.26-2.64-.39-3.39-.39-3.2 0-8.32 1.22-19.74 4.48-12.35 3.53-16.3 4.52-19.26 4.52l-2.36-.02c-7.94-.1-12.85.17-18.47 1.42-4.97 1.11-9.3 2.9-12.88 5.5a21.4 21.4 0 0 1-3.75 2.22c-3.32 1.5-6 1.87-13.04 2.09-14.25.44-23.13 1.5-39.37 5.77a125.56 125.56 0 0 0-11.95 3.7c-7.17 2.7-11.49 5.46-13.93 8.68-1.9 2.52-2.58 4.76-3.33 9.8-.9 6.08-1.53 8.68-3.47 12.56a30.6 30.6 0 0 1-9.66 11.45c-3.12 2.26-5.95 3.73-11.93 6.4l-2.31 1.04c-5.01 2.27-8.18 3.99-11.25 6.29-12.9 9.68-15.93 14.17-31.85 48.8-6.31 13.76-12.7 22.68-19.6 28.25-5.08 4.1-8.53 5.57-17.3 8.27a44.64 44.64 0 0 0-7.33 2.73c-6.24 3.12-15.7 6.4-28.3 10.06a867.4 867.4 0 0 1-21.8 5.97c-3.77 1.01-7.93 3.1-12.56 6.19a137.35 137.35 0 0 0-12.95 10.07c-2.24 1.92-9.48 8.3-9.48 8.3a98.2 98.2 0 0 1-7.84 6.37c-2.46 1.72-15.32 9.83-19.26 12.5a203 203 0 0 0-26.69 21.45c-11.13 10.58-12.43 18.3-7.47 28.63a74.52 74.52 0 0 0 2.62 4.95l.94 1.7a69.84 69.84 0 0 1 4.03 8.17c2.88 7.2 3.4 12.46 1.89 16.73-1.22 3.43-3.28 5.77-8.02 9.84-1.14.97-2.32 1.8-5.3 3.67-3.92 2.45-5.69 3.89-7.31 6.42-2.13 3.3-3.22 7.89-3.22 14.53 0 9.05-1.34 15.79-4.05 21.34-2.19 4.49-4.85 7.77-10.1 13.01-7.07 7.07-11.85 28.9-11.85 48.65 0 2.8-.58 5.48-1.7 8zm282.54 0h-1.01l-1.1-5.8c-3.08-16.26-4.05-26.2-2.74-37.26.7-5.8.77-9.68.55-15.3-.18-4.45-.17-5.68.19-7.63.78-4.3 3.44-8.53 10.39-16.34 9.07-10.2 12.26-15.41 19.8-30.15 1.35-2.64 2.33-4.47 3.38-6.3.9-1.58 1.82-3.06 2.77-4.5 3.14-4.7 7.03-8.42 16.84-16.81 11.22-9.6 15.5-13.86 18.13-19.13.7-1.4 1.3-2.8 1.93-4.4a206 206 0 0 0 1.49-4.05c3.63-9.94 8.01-13.93 22.9-17.81 4.99-1.3 20.55-5.13 21.38-5.34 16.19-4.1 25.33-7.36 33.48-12.6 5.86-3.77 5.84-3.76 27.66-16.53l2.6-1.52c10.23-6 17.1-10.2 22.73-13.95a149.3 149.3 0 0 0 8.8-6.3 723.7 723.7 0 0 0 6.37-5.08A87.74 87.74 0 0 1 600 342.95v1.12a85.76 85.76 0 0 0-15.49 9.9c.18-.14-4.76 3.84-6.38 5.1a150.3 150.3 0 0 1-8.85 6.35c-5.65 3.76-12.53 7.96-22.78 13.97l-2.6 1.53c-21.8 12.75-21.78 12.74-27.63 16.5-8.27 5.32-17.49 8.61-33.78 12.73-.83.21-16.39 4.04-21.36 5.33-8.03 2.1-13.15 4.5-16.45 7.5-2.66 2.42-4 4.86-5.77 9.7l-1.5 4.07a51.12 51.12 0 0 1-1.96 4.47c-2.72 5.45-7.04 9.75-18.38 19.45-9.73 8.32-13.6 12.02-16.65 16.6a77.18 77.18 0 0 0-2.74 4.45c-1.05 1.81-2.01 3.63-3.35 6.25-7.58 14.81-10.82 20.08-19.96 30.36-6.83 7.7-9.4 11.78-10.15 15.86-.34 1.85-.34 3.04-.17 7.4.22 5.68.14 9.6-.55 15.47-1.3 10.92-.34 20.79 2.73 36.95l1.12 5.99zm-76.59 0h-2.1l1.39-4.3c1.04-3.3 1.93-6.78 2.68-10.4 2.65-12.73 3.27-23.63 3.27-41.3 0-5.71-1.86-9.75-4.13-9.75-2.94 0-6.96 5.61-10.93 17.08C271.14 579.68 258.3 593 238 593c-22.42 0-29.26-1.35-48.42-10.09a87.69 87.69 0 0 1-9.42-5.04c-2.95-1.8-12.78-8.57-14.84-9.72-4.2-2.36-7-2.71-9.72-.99-.63.4-1.26.91-1.9 1.55a57.69 57.69 0 0 1-4.31 3.86 147.88 147.88 0 0 1-3.06 2.44l-1 .8C137.01 582.43 134 587.18 134 597c0 1.02-.02 2.01-.07 3h-2c.05-.99.07-1.98.07-3 0-10.52 3.33-15.78 12.09-22.76a265.61 265.61 0 0 1 2-1.6c.83-.64 1.43-1.13 2.03-1.61a55.76 55.76 0 0 0 4.17-3.74c.74-.73 1.48-1.34 2.24-1.82 3.47-2.2 7-1.75 11.77.93 2.15 1.21 12.03 8 14.9 9.76a85.7 85.7 0 0 0 9.22 4.93C209.29 589.7 215.85 591 238 591c19.25 0 31.49-12.7 41.06-40.33 4.24-12.25 8.66-18.42 12.81-18.42 3.8 0 6.13 5.06 6.13 11.75 0 17.8-.63 28.8-3.3 41.7-.77 3.7-1.68 7.23-2.75 10.6-.4 1.3-.8 2.53-1.19 3.7zm-149.25 0l.5-.94a160.1 160.1 0 0 0 6.53-13.26c2.73-6.29 5.78-9.64 9.24-10.52 3.74-.95 7.15.74 12.56 5.13 5.43 4.4 6.07 4.86 7.73 5.1 1.6.22 4.28 1.14 8.86 2.95 1.3.5 10.78 4.35 13.85 5.55 3.07 1.2 5.85 2.25 8.49 3.18 3.1 1.1 5.98 2.04 8.65 2.81h-3.45c-1.76-.56-3.6-1.18-5.54-1.87a281.2 281.2 0 0 1-8.51-3.19c-3.08-1.2-12.57-5.04-13.86-5.55-4.5-1.78-7.15-2.68-8.63-2.9-1.94-.27-2.53-.7-8.22-5.3-5.17-4.2-8.36-5.78-11.69-4.94-3.1.78-5.94 3.92-8.56 9.95a161 161 0 0 1-6.82 13.8h-1.13zm112.89 0a30.34 30.34 0 0 0 11.27-6.27c1.55-1.36 3.32-3.46 5.34-6.29 1.05-1.46 2.15-3.1 3.41-5.04a349.73 349.73 0 0 0 2.5-3.9l.47-.75.93-1.47a89.17 89.17 0 0 1 3.25-4.86c1.05-1.43 1.82-2.23 2.44-2.46 1.02-.37 1.49.48 1.49 2.04l.01 2.11c.05 6.91-.08 11.32-.7 16.33a48.4 48.4 0 0 1-2.38 10.56h-1.07a46.47 46.47 0 0 0 2.45-10.68c.62-4.96.75-9.33.7-16.2l-.01-2.12c0-.97-.08-1.12-.15-1.1-.36.14-1.05.85-1.97 2.1a88.44 88.44 0 0 0-3.22 4.82l-.92 1.46-.48.75a1268.1 1268.1 0 0 1-2.5 3.92c-1.26 1.95-2.38 3.6-3.44 5.08-2.06 2.88-3.87 5.04-5.5 6.45a30.87 30.87 0 0 1-8.94 5.52h-2.98zm-183.72 0H69.3c3.37-3.43 5.19-8.33 5.19-15 0-18.6-.04-17.35 1.02-20.77.6-1.93 1.5-3.74 3.27-6.63.42-.7 4.92-7.8 6.78-10.86 3.04-4.97 11.04-16.5 12.21-18.56 3.48-6.08 4.72-12.06 4.72-24.18 0-7.85 2.5-14.2 8.1-23.44l2.84-4.63a72.67 72.67 0 0 0 2.49-4.4c1.62-3.15 2.48-5.78 2.62-8.28.2-3.78-1.3-7.29-4.9-10.9-5.13-5.12-8.6-5.43-11.2-1.85-2.12 2.92-3.48 7.74-5.06 16.47-.2 1.03-.82 4.6-.82 4.57-.83 4.67-1.4 7.33-2.1 9.6-1.35 4.42-3.7 7.61-8.36 12.26l-3.26 3.2c-6.38 6.39-9.68 11.51-11.36 19.5l-1.16 5.52c-.87 4.1-1.56 7.04-2.33 9.94-3.67 13.74-9.65 25.97-22.59 44.72-7.68 11.14-11.05 18.87-10.92 23.72h-1c-.12-5.16 3.35-13.05 11.1-24.28 12.87-18.67 18.8-30.8 22.44-44.42.77-2.88 1.45-5.8 2.32-9.89l1.16-5.51c1.73-8.22 5.13-13.5 11.64-20 .63-.64 2.84-2.8 3.25-3.21 4.57-4.54 6.82-7.62 8.12-11.84a81.58 81.58 0 0 0 2.07-9.48l.81-4.57c1.62-8.9 3-13.8 5.24-16.89 3-4.15 7.2-3.78 12.71 1.74 3.8 3.8 5.42 7.58 5.2 11.66-.15 2.66-1.05 5.41-2.73 8.68a73.6 73.6 0 0 1-2.52 4.46l-2.84 4.63c-5.52 9.1-7.96 15.3-7.96 22.92 0 12.28-1.28 18.43-4.85 24.68-1.2 2.1-9.21 13.65-12.22 18.58-1.87 3.06-6.37 10.18-6.78 10.86-1.73 2.82-2.6 4.57-3.17 6.4-1.02 3.28-.98 2.1-.98 20.48 0 6.52-1.7 11.44-4.82 15zM310.09 0h1.06c-.37.9-.77 1.83-1.2 2.82-3.9 9.06-5.45 15.15-5.45 25.18 0 7.64-2.1 11.6-6.64 13.05-3.46 1.1-5.72.98-17.57-.43-11.55-1.36-19.17-1.58-28.16-.14-6.24 2.49-25.91 7.02-32.13 7.02-11.15 0-36.76-2.88-54.12-7.01a22.08 22.08 0 0 0-16.95 2.48c-4.05 2.33-7.09 5.03-13.9 11.97-6.28 6.39-9.53 9.23-13.8 11.5-7.09 3.79-11.22 7.65-13.4 12.27-1.82 3.85-2.33 7.84-2.33 15.29 0 4.4-2.65 6.69-9.45 9.74.1-.05-2.97 1.31-3.84 1.71-8.78 4.06-12.71 8.29-12.71 16.55 0 12.52-4.86 19.22-17.34 27.96l-4.56 3.14c-1.9 1.3-3.3 2.3-4.67 3.3-.92.68-1.79 1.34-2.62 2-7.16 5.62-11 14.54-15.56 33.28-.63 2.57-3.3 14-4.07 17.14a350.44 350.44 0 0 1-5.2 19.33c-1.37 4.5-4.5 15.07-4.96 16.53-1.05 3.4-1.64 4.94-2.46 6.32-.82 1.4-6.85 9.08-12.64 18.27L0 277.98v-1.9l4.58-7.35a270.8 270.8 0 0 1 12.61-18.23c-.3.5 1.35-2.8 2.38-6.12.45-1.44 3.58-12.01 4.95-16.53 1.83-6.03 3.44-12.09 5.19-19.27.76-3.13 3.44-14.56 4.06-17.14 4.62-18.95 8.52-28.02 15.92-33.83.84-.67 1.72-1.33 2.65-2.01 1.38-1.02 2.8-2.01 4.7-3.32l4.54-3.14C73.83 140.57 78.5 134.13 78.5 122c0-8.74 4.2-13.26 13.29-17.45.88-.41 3.96-1.77 3.85-1.73 6.46-2.9 8.86-4.97 8.86-8.82 0-7.6.53-11.7 2.42-15.71 2.29-4.84 6.57-8.85 13.84-12.73 4.15-2.21 7.35-5 14.15-11.93 6.28-6.4 9.36-9.13 13.52-11.53a23.07 23.07 0 0 1 17.69-2.59c17.27 4.12 42.8 6.99 53.88 6.99 6.1 0 25.73-4.53 31.92-7 9.12-1.46 16.83-1.25 28.49.13 11.63 1.38 13.9 1.5 17.15.47 4.06-1.3 5.94-4.85 5.94-12.1 0-10.1 1.56-16.3 6.6-28zm25.12 0h1c.05 5.62.26 11.48.65 19.4.47 9.7.64 14.57.64 21.6 0 9.81-4.68 17.46-13.1 23.16-6.53 4.43-14.94 7.46-24.33 9.33-3.74.54-9.42.56-22.68.23-6.74-.17-9.35-.22-12.39-.22-2.77 0-4.97.43-7.63 1.36-.88.3-4.55 1.74-5.58 2.11-6.55 2.35-13.59 3.53-24.79 3.53-8.1 0-13.58-1.38-22.46-4.9l-3.18-1.25c-12.55-4.87-21.27-5.15-37.18 1.12-11.15 4.39-18.13 9.2-22.28 14.81-3.15 4.26-4.33 7.8-5.94 15.8-1.22 6.09-1.93 8.74-3.5 12.13-1.65 3.53-3.97 5.81-7.07 7.22-2.33 1.07-4.35 1.5-9.32 2.19-9.04 1.27-12.77 3.09-15.61 9.58-3.71 8.48-7.72 13.87-14.22 19.76-2.4 2.18-13.14 11.02-15.91 13.42-8.2 7.1-13.85 17.37-18.7 31.97a258.81 258.81 0 0 0-3.27 10.7c-.01.05-2.26 7.97-2.88 10.1-8.49 28.85-17.88 52.95-26.13 61.2-2.8 2.8-5.06 5.64-10.4 12.96-3.4 4.68-6.23 8.25-8.95 11.1v-1.55c2.74-2.98 5.73-6.82 9.48-11.97 4.03-5.52 6.32-8.4 9.17-11.24 8.07-8.08 17.44-32.14 25.87-60.8.62-2.1 2.86-10.03 2.88-10.08 1.21-4.24 2.21-7.53 3.28-10.74 4.9-14.75 10.63-25.16 19-32.4 2.78-2.42 13.5-11.25 15.89-13.4 6.4-5.8 10.32-11.09 13.97-19.43 1.68-3.83 4.05-6.31 7.2-7.86 2.4-1.17 4.64-1.67 9.53-2.36 4.54-.63 6.5-1.05 8.7-2.06 2.89-1.31 5.03-3.42 6.58-6.73 1.53-3.3 2.23-5.9 3.43-11.9 1.64-8.14 2.85-11.79 6.11-16.2 4.28-5.79 11.41-10.7 22.73-15.16 16.15-6.36 25.13-6.07 37.9-1.11l3.19 1.26c8.77 3.47 14.13 4.82 22.09 4.82 11.09 0 18.02-1.16 24.46-3.47 1-.36 4.68-1.8 5.58-2.11A22.5 22.5 0 0 1 265 72.5c3.05 0 5.67.05 14.07.26 11.53.29 17.2.27 20.83-.25 9.25-1.85 17.54-4.83 23.94-9.17C332 57.8 336.5 50.46 336.5 41c0-7-.17-11.86-.7-22.7-.35-7.26-.55-12.83-.59-18.3zM93.87 0h2.04c-.7 4-1.61 6.82-3.03 9.47-2.33 4.38-2.85 5.75-5.26 13.03a40.46 40.46 0 0 1-1.94 5.03c-2.24 4.66-5.92 8.8-13.07 14.26-8.01 6.13-14.27 16.55-20.03 31.55-2.4 6.23-8.75 25.63-9.64 28.01-2.69 7.16-6.56 12.7-15.63 23.68l-2.68 3.24c-6.02 7.34-9.35 12.07-11.72 17.15-2.3 4.94-7.12 9.9-12.91 14.15v-2.4c5.14-3.94 9.1-8.3 11.1-12.6 2.46-5.27 5.87-10.1 11.98-17.56l2.68-3.26c8.94-10.8 12.72-16.22 15.3-23.1.88-2.33 7.24-21.74 9.65-28.03 5.89-15.31 12.3-26 20.68-32.41 6.92-5.3 10.4-9.2 12.48-13.55.65-1.35 1.16-2.7 1.85-4.79 2.45-7.4 3-8.83 5.4-13.34A27.68 27.68 0 0 0 93.87 0zm9.07 0h1.02c-1.66 8.3-2.91 12.67-4.54 15.26a59.14 59.14 0 0 0-4.1 8.21c-1.27 3-2.44 6.2-3.5 9.4-.38 1.12-.7 2.16-2.41 5.39a251.48 251.48 0 0 0-12.81 13.3c-3.48 3.96-5.95 7.27-7.15 9.66-.95 1.9-2.06 5.99-3.61 12.97-.64 2.9-3.65 17.15-4.51 21.07-3.63 16.45-6.63 26.69-9.9 32-7.66 12.45-10.64 15.71-37.08 41.1A69.78 69.78 0 0 1 0 179.21v-1.15a69.39 69.39 0 0 0 13.65-10.42c26.4-25.33 29.32-28.55 36.92-40.9 3.2-5.18 6.18-15.37 9.78-31.7.86-3.91 3.87-18.16 4.51-21.06 1.57-7.09 2.7-11.2 3.7-13.2 1.24-2.5 3.76-5.86 7.29-9.89.9-1.03 1.86-2.1 2.86-3.18 2.4-2.6 4.96-5.22 7.53-7.76.9-.88 1.73-1.7 3.37-3.4a129.02 129.02 0 0 1 4.78-13.46 60.07 60.07 0 0 1 4.19-8.35c1.52-2.44 2.74-6.71 4.36-14.74zM83.71 0h1.1c-2.09 4.74-6.03 8.92-11.42 12.3-7.2 4.52-16.5 7.2-24.39 7.2-8.9 0-11.8 7-11.74 21.52 0 1.7.04 3.17.12 5.99.1 3.3.12 4.45.12 5.99 0 5.73-.76 11.3-2.01 16.5a66.67 66.67 0 0 1-2.15 6.97 2597.76 2597.76 0 0 1-7 15.86A4270.8 4270.8 0 0 1 6.44 136.2 54.64 54.64 0 0 1 0 147v-1.65a54.87 54.87 0 0 0 5.55-9.57A4269.82 4269.82 0 0 0 30.7 79.97c.53-1.2.99-2.23 2.44-5.9A69.23 69.23 0 0 0 36.5 53c0-1.52-.03-2.66-.12-5.95-.08-2.83-.12-4.31-.12-6.01-.03-6.79.53-11.62 2.07-15.34 1.94-4.68 5.39-7.19 10.67-7.19 7.7 0 16.81-2.63 23.86-7.05C77.93 8.27 81.66 4.38 83.7 0zm282.63 0h1.01c1.86 10.02 2.18 12.67 2.32 18.3a123.43 123.43 0 0 1 .37 27.83c-.96 8.78-3.1 16.01-6.63 21.15-11.34 16.5-39.8 29.22-66.41 29.22-5.09 0-10.47.28-16.31.83a413.8 413.8 0 0 0-24.37 3.16c-21.56 3.26-27.66 4.01-36.32 4.01-6.92 0-12.2-1.05-21.69-3.9l-2.78-.83c-1.39-.41-2.54-.74-3.65-1.02-8-2.05-14.22-2.04-21.7.72a16.32 16.32 0 0 0-9.17 8.18c-1.6 3.05-2.5 6.06-4.02 12.83-1.5 6.64-2.34 9.52-3.99 12.64a16.16 16.16 0 0 1-9.85 8.36 104.8 104.8 0 0 0-9.5 3.42c-6.55 2.8-10.1 5.57-13.8 10.47-1.33 1.75-1.03 1.3-5.43 7.9-1.98 2.97-4.66 5.8-8.48 9.14-2.01 1.76-10.71 8.83-12.88 10.7-7.37 6.35-12.58 12.14-16.63 19.14-4.22 7.3-7.8 18.3-11.28 33.26-.87 3.73-1.72 7.64-2.64 12.14l-1.18 5.8-1.09 5.45c-1.8 8.96-2.77 13.28-3.77 16.26-6.8 20.44-17.26 42.16-27.13 51.2-5.11 4.7-8.1 7.07-11.1 8.86-.9.54-1.84 1.04-2.92 1.57-.44.22-9.6 4.4-14.1 6.66l-1.22.62v-1.13l.78-.39c4.52-2.26 13.67-6.44 14.1-6.65a41.19 41.19 0 0 0 2.84-1.54c2.94-1.75 5.88-4.09 10.94-8.73 9.71-8.9 20.1-30.51 26.87-50.79.97-2.92 1.94-7.22 3.73-16.13l1.1-5.46a490.5 490.5 0 0 1 3.82-17.96c3.5-15.06 7.1-26.14 11.39-33.54 4.11-7.11 9.4-12.98 16.83-19.4 2.19-1.88 10.88-8.95 12.88-10.7 3.77-3.28 6.39-6.05 8.3-8.93 4.43-6.64 4.12-6.18 5.47-7.96 3.8-5.03 7.5-7.91 14.21-10.78 2.61-1.12 5.74-2.24 9.59-3.46a15.17 15.17 0 0 0 9.27-7.86c1.59-3.02 2.42-5.85 4.03-12.99 1.41-6.27 2.32-9.33 3.98-12.48a17.31 17.31 0 0 1 9.7-8.66c7.7-2.83 14.1-2.84 22.3-.75 1.12.29 2.28.61 3.68 1.03l3.73 1.11c8.47 2.54 13.66 3.58 20.46 3.58 8.59 0 14.67-.75 36.18-4a414.64 414.64 0 0 1 24.41-3.17c5.88-.54 11.29-.83 16.41-.83 26.3 0 54.45-12.58 65.59-28.78 3.42-4.98 5.5-12.06 6.46-20.7.84-7.74.73-16.02.02-23.9a136.2 136.2 0 0 0-.57-5.12c0-4.47-.3-6.94-2.16-17zM18.88 0h1.03C18 7.57 17.15 10.18 14.46 16.2c-1.95 4.37-2.67 9.19-2.42 14.89.2 4.33.71 7.7 2.28 16.13 1.09 5.88 1.57 8.77 1.94 12.2.96 8.9.24 16.08-2.8 22.79A463.4 463.4 0 0 1 0 109.43v-2.12a465 465 0 0 0 12.54-25.52c2.97-6.52 3.67-13.53 2.72-22.27-.36-3.4-.84-6.26-1.93-12.12-1.57-8.47-2.1-11.88-2.29-16.27-.26-5.84.48-10.81 2.5-15.33 2.64-5.9 3.48-8.47 5.34-15.8zm280.47 0a70.78 70.78 0 0 1-4.91 11.24c-2.56 4.7-4.01 8.45-4.86 11.98l-.4 1.8-.28 1.45a5.28 5.28 0 0 1-.74 2.07c-.74 1.03-1.93 1.28-5.13 1.25.92 0-9.85-.29-15.03-.29-10.2 0-18.45.82-29.46 2.56-16.87 2.66-17.73 2.77-23.66 2.52a42.57 42.57 0 0 1-8-1.09c-17.7-4.16-46.18-5.86-54.72-3.01-2.72.9-5.88 2.8-9.52 5.59a112.37 112.37 0 0 0-6.54 5.48c-1.4 1.25-9.17 8.5-10.78 9.84-1.45 1.2-8.18 7.42-8.85 8.02a114.65 114.65 0 0 1-4.55 3.9c-4.99 4.03-8.9 6.2-11.92 6.2-3.52.05-4.32 0-5.14-.4-1.13-.56-1.5-1.72-1.13-3.57.74-3.63 4.47-10.84 12.84-24.8 5.69-9.48 9.42-18 11.78-26.2 1.45-5.04 1.94-7.4 2.97-14.54h1.01c-1.05 7.3-1.54 9.7-3.01 14.82-2.39 8.28-6.16 16.89-11.9 26.44-8.3 13.84-12 21.01-12.7 24.48-.3 1.45-.08 2.14.59 2.47.6.3 1.35.35 3.48.3 3.92 0 7.69-2.1 12.5-5.98 1.4-1.13 2.87-2.39 4.51-3.86.66-.59 7.41-6.83 8.88-8.05 1.59-1.33 9.34-8.55 10.75-9.82 2.4-2.15 4.55-3.96 6.6-5.53 3.72-2.85 6.97-4.8 9.81-5.74 8.76-2.92 37.41-1.22 55.27 2.99 2.57.6 5.14.95 7.81 1.06 5.84.25 6.7.14 23.47-2.51 11.05-1.75 19.36-2.57 29.6-2.57 5.2 0 15.99.3 15.05.29 2.87.03 3.84-.17 4.3-.83.23-.32.4-.8.58-1.7l.28-1.43.4-1.85c.88-3.6 2.36-7.44 4.96-12.22 1.87-3.43 3.44-7 4.73-10.76h1.06zm-8.59 0c-5.91 17.94-9.55 22-19.76 22-4.5 0-10.22.32-28.69 1.5l-1.53.1c-15.6.99-23.47 1.4-28.78 1.4-5.35 0-13.24-.96-28.86-3.28l-1.54-.23C163.18 18.75 157.47 18 153 18c-4.45 0-7.3 1.01-10.96 3.34-.1.06-1.8 1.17-2.3 1.47-2.43 1.5-4.32 2.19-6.74 2.19-2.8 0-4.11-1.46-4.11-4.22 0-1.04.16-2.29.5-4.1.16-.82.9-4.4 1.07-5.32.8-4.11 1.3-7.68 1.47-11.36h2c-.17 3.82-.68 7.5-1.5 11.75-.19.94-.92 4.5-1.07 5.31a21.04 21.04 0 0 0-.47 3.72c0 1.7.46 2.22 2.11 2.22 1.99 0 3.55-.57 5.7-1.9.47-.28 2.15-1.37 2.26-1.44C144.92 17.14 148.12 16 153 16c4.62 0 10.3.74 28.9 3.51l1.53.23C198.93 22.04 206.8 23 212 23c5.25 0 13.11-.41 28.65-1.4l1.54-.1C260.73 20.32 266.43 20 271 20c8.95 0 12.15-3.4 17.66-20h2.1zM141.51 0h1.13c-2.06 3.86-2.63 5.1-2.77 6.19-.15 1.12.42 1.64 2.32 1.96 1.8.3 3.85.35 10.81.35 6.02 0 13 .56 21.35 1.62 3.95.5 8.03 1.1 13.13 1.89 24 3.7 22.5 3.49 26.83 3.49 24.02 0 51.83-2.24 60.45-6.94 2.88-1.57 5.05-4.49 6.6-8.56h1.07c-1.64 4.47-3.98 7.69-7.2 9.44-8.83 4.82-36.67 7.06-60.92 7.06-4.41 0-2.84.22-26.98-3.5-5.1-.8-9.17-1.38-13.1-1.88-8.31-1.06-15.26-1.62-21.23-1.62-7.04 0-9.1-.05-10.97-.37-2.38-.4-3.38-1.32-3.15-3.07.16-1.22.69-2.41 2.63-6.06zm76.4 0c5.69 1.64 10.37 2.5 14.09 2.5 9.59 0 16.7-.71 22.4-2.5h2.98C251.12 2.53 243.2 3.5 232 3.5c-4.5 0-10.32-1.21-17.53-3.5h3.45zM70.69 0c-2.87 3.27-6.95 5.39-12.02 6.53-3.98.89-7.5 1.08-12.92 1A97.24 97.24 0 0 0 44 7.5c-5.37 0-8.86-1.24-10.1-4.97A8.6 8.6 0 0 1 33.5 0h.99c.02.82.14 1.56.36 2.22C35.91 5.39 39.02 6.5 44 6.5l1.76.02c5.35.09 8.8-.1 12.69-.97C62.95 4.54 66.63 2.74 69.3 0h1.37zM0 207.87c7.31-.16 11.5 3.33 11.5 11.13 0 11.41-5.05 28.35-11.5 41.5v-2.3c5.93-12.72 10.5-28.47 10.5-39.2 0-7.18-3.7-10.3-10.5-10.13v-1zm0 7.05c1.23.14 2.18.58 2.87 1.31 1.4 1.48 1.6 3.72 1.16 7.58l-.16 1.3A28.93 28.93 0 0 0 3.5 229c0 3.2-1.48 9.52-3.5 15.9v-3.45c1.49-5.13 2.5-9.87 2.5-12.45 0-.98.08-1.75.37-4.02l.16-1.29c.42-3.56.24-5.59-.88-6.77-.5-.53-1.21-.87-2.15-1v-1zM0 410.9v-1.47a21.67 21.67 0 0 0 2.97-4.7c1.32-2.7 2.68-6.28 4.56-11.89 7.85-23.55 7.83-26.6.25-30.4-2.25-1.12-4.8-1.43-7.78-.91v-1.02a13.1 13.1 0 0 1 8.22 1.04c8.24 4.12 8.26 7.6.25 31.6-1.88 5.66-3.25 9.27-4.6 12.02A20.82 20.82 0 0 1 0 410.9zM33.64 452c1.68 0 3.04-.23 8.34-1.31l2.38-.47c8.26-1.57 12.72-1.3 14.53 2.33 1.38 2.75-.47 5.86-4.75 9.68a75.6 75.6 0 0 1-5.08 4.07c-.94.7-4.89 3.59-5.79 4.27-1.86 1.4-2.97 2.37-3.47 3.03a19.08 19.08 0 0 0-2.89 5.5c.07-.2-4.02 13.65-6.96 22.22-2.7 7.85-5.56 10.72-8.82 8.59-2.11-1.4-3.66-4.24-6.6-11.03-1.98-4.62-2.5-5.76-3.4-7.4-4.55-8.18-3.9-23.9-.05-32.87a9.6 9.6 0 0 1 6.98-5.96c2.59-.66 4.86-.75 11.78-.67l3.8.02zm0 2c-1.13 0-2.09 0-3.82-.02-12.07-.13-14.83.57-16.9 5.41-3.63 8.47-4.26 23.55-.05 31.12.96 1.73 1.48 2.88 3.5 7.58 2.72 6.3 4.24 9.08 5.86 10.14 1.64 1.08 3.5-.8 5.82-7.55a682.9 682.9 0 0 0 6.97-22.24 21.03 21.03 0 0 1 3.18-6.04c.65-.87 1.85-1.9 3.86-3.43.92-.7 4.87-3.57 5.8-4.27 2.02-1.5 3.6-2.77 4.95-3.97 3.63-3.23 5.09-5.7 4.3-7.28-1.21-2.42-5.07-2.65-12.38-1.27l-2.35.47c-5.49 1.11-6.86 1.35-8.74 1.35zm345.63 146c-3.45-12.26-3.77-14.13-3.77-19 0-3.33-.13-6.27-.43-11.34-.63-10.33-.65-13.5.26-17.07 1.21-4.74 4.21-7.1 9.67-7.1h26c4.08 0 5.19 1.85 5.93 7.11.1.79.13.97.19 1.32.84 5.35 2.8 7.58 8.88 7.58 3.64 0 5.54.4 6.43 1.37.76.83.76 1.44.36 3.93-.85 5.26.5 8.85 7.5 13.8 6.32 4.45 11.63 5.36 16.55 3.37 3.8-1.54 6.73-4.16 11.92-10l1.1-1.23 1.09-1.23a75.6 75.6 0 0 1 2.7-2.86 35.81 35.81 0 0 1 9.57-6.73c1.52-.76 1.72-.86 5.66-2.63 6.1-2.73 9.01-4.5 11.74-7.62 2.63-3 4.67-4.85 6.7-6.04 3.18-1.85 5.46-2.13 13.68-2.13 5.98 0 10.56-4.32 18-14.99l2.82-4.03c1.06-1.5 1.94-2.7 2.79-3.79 7.87-10.12 19.38-10.4 30.74.96 5.54 5.53 10.17 19.43 13.64 38.51 2.5 13.75 4.18 29.46 4.47 39.84h-1c-.3-10.32-1.96-25.97-4.45-39.66-3.43-18.87-8.02-32.65-13.36-37.99-10.95-10.95-21.76-10.68-29.26-1.04-.83 1.07-1.7 2.26-2.75 3.75l-2.81 4.02c-7.65 10.95-12.38 15.42-18.83 15.42-8.04 0-10.21.26-13.17 2-1.92 1.12-3.9 2.9-6.45 5.83-2.86 3.26-5.87 5.09-12.09 7.88a103.35 103.35 0 0 0-5.62 2.6 34.84 34.84 0 0 0-9.32 6.54 74.67 74.67 0 0 0-3.75 4.05l-1.1 1.24c-5.28 5.95-8.29 8.64-12.28 10.25-5.26 2.13-10.92 1.17-17.5-3.48-7.33-5.17-8.82-9.15-7.92-14.77.34-2.12.34-2.6-.1-3.1-.64-.69-2.34-1.04-5.7-1.04-6.63 0-8.96-2.63-9.87-8.42l-.2-1.34c-.67-4.82-1.53-6.24-4.93-6.24h-26c-5 0-7.6 2.04-8.7 6.34-.88 3.43-.85 6.57-.23 16.76a177 177 0 0 1 .43 11.4c0 4.78.32 6.63 3.81 19h-1.04zm13.68 0c-1.31-6.58-1.61-10.71-1.36-14.84.04-.7.1-1.44.18-2.38l.23-2.56c.34-3.81.5-6.97.5-11.22 0-4.94 1.46-7.76 4.21-8.42 2.38-.58 5.56.54 9.2 3 6.64 4.52 13.99 13.07 16.55 19.23 4.77 11.44 14.12 15.69 33.54 15.69 8.6 0 14.32-2.35 20.67-7.88 1.45-1.26 15.06-15 21-20 7.21-6.07 11.77-7.59 20.62-8.32 5.52-.45 7.98-.9 11.44-2.36 4.58-1.95 9.36-5.48 14.9-11.29 7.43-7.76 13.25-8.92 17.47-4.3 3.32 3.63 5.46 10.58 6.82 20.24.73 5.17.94 7.74 1.58 17.38.25 3.75.17 5.32-.92 18.03h-1c1.09-12.7 1.17-14.28.92-17.97-.64-9.6-.85-12.16-1.57-17.3-1.33-9.47-3.43-16.27-6.56-19.7-3.76-4.11-8.93-3.08-16 4.32-5.65 5.9-10.54 9.5-15.25 11.5-3.58 1.53-6.13 1.99-11.6 2.44-8.8.72-13.17 2.18-20.2 8.1-5.9 4.96-19.5 18.7-21 19.99-6.52 5.68-12.47 8.12-21.32 8.12-19.78 0-29.5-4.42-34.46-16.3-2.49-5.97-9.71-14.38-16.2-18.79-3.42-2.32-6.36-3.35-8.4-2.86-2.2.53-3.44 2.92-3.44 7.45 0 4.28-.16 7.47-.5 11.31l-.23 2.56c-.09.93-.14 1.65-.19 2.35-.24 4.08.06 8.18 1.39 14.78h-1.02zm113.75 0c2.52-3.26 8.93-11.79 10.9-14.3 5.48-6.98 13.05-12.38 19.4-13.94 7.01-1.71 11.5 1.45 11.5 9.24 0 4.02-.04 5.16-.74 19h-1c.7-13.85.74-15 .74-19 0-7.12-3.86-9.83-10.26-8.26-6.11 1.5-13.5 6.77-18.85 13.57-1.86 2.36-7.65 10.07-10.43 13.69h-1.26zm-9.86-338.96c3.44 2.71 7 5.1 11.44 7.75 1.06.64 8.42 4.9 10.35 6.1 11.27 7 15 13.35 12.35 25.33-1.45 6.52-4.53 11.1-9.39 14.44-3.83 2.63-8.07 4.26-16.08 6.56-11.97 3.45-13.68 3.99-18.82 6.28a60.18 60.18 0 0 0-7.81 4.18c-11.11 7.07-19.1 7.7-27.96 3.28-3.56-1.77-17.2-11-17.2-11.01a101.77 101.77 0 0 0-5.2-3.07c-16.04-8.83-34.27-24.16-34.52-31.85-.11-3.46 1.99-6.57 6.28-10.26 1.03-.9 2.18-1.81 3.68-2.95.72-.55 3.38-2.56 3.94-3 4.47-3.4 7.18-5.79 9.32-8.45 11.12-13.82 26.55-28.68 34.36-32.28 12.06-5.54 19.84-5.77 27.37.12 3.25 2.54 5.65 6.54 8.58 13.35.29.65 2.3 5.45 2.88 6.74 1.62 3.65 2.9 5.8 4.24 6.94.72.6 1.45 1.2 2.2 1.8zm-3.49-.28c-1.63-1.39-3.03-3.74-4.77-7.65-.58-1.3-2.6-6.12-2.88-6.76-2.81-6.5-5.08-10.3-7.98-12.56-6.83-5.35-13.85-5.15-25.3.12-7.45 3.42-22.7 18.12-33.64 31.72-2.27 2.82-5.08 5.3-9.67 8.79l-3.94 2.98a79.98 79.98 0 0 0-3.59 2.88c-3.87 3.33-5.67 6-5.58 8.69.21 6.64 18.14 21.72 33.48 30.15 1.76.97 3.5 2 5.3 3.13.12.08 13.61 9.22 17.03 10.92 8.22 4.1 15.46 3.52 26-3.18a62.17 62.17 0 0 1 8.07-4.31c5.25-2.35 7-2.9 19.08-6.38 7.8-2.24 11.9-3.82 15.5-6.3 4.44-3.04 7.23-7.18 8.56-13.22 2.44-11.02-.83-16.6-11.45-23.2-1.9-1.18-9.23-5.42-10.32-6.08-4.5-2.69-8.13-5.12-11.64-7.9-.77-.6-1.52-1.21-2.26-1.84zM87.72 241.6c4.3-2.98 7.88-5 12.14-6.95.84-.4 1.73-.78 2.78-1.24l4.37-1.88a164.3 164.3 0 0 0 17.74-8.96 320.67 320.67 0 0 1 27.87-14.5c4.22-1.95 21.89-9.84 21.17-9.52 19.17-8.62 28.1-6.93 49.5 8.05 7.91 5.54 13.24 13.25 16.45 22.66 3.02 8.83 3.76 16.51 3.76 27.75 0 8.32-.66 12.95-3.68 18.97-4.18 8.36-12.3 16.14-25.58 23.47-24.45 13.49-38.83 27.55-52.83 47.84-8.83 12.8-47.76 44.21-65.16 54.15C75.04 413.55 48.89 423.5 31 423.5c-10.05 0-14.67-4.78-14.76-13.37-.07-6.32 2.06-13.73 6.3-24.32 2.95-7.37 2.02-12.9-2.16-22.29-3.19-7.17-3.88-9.14-3.88-12.52 0-3.35 1.87-6.9 5.52-11.07 2.61-3 3.5-3.83 11.9-11.5 5.09-4.66 8.08-7.6 10.7-10.75 9.46-11.36 12.62-19.47 17.9-44.78 3.12-15.05 6.63-20.28 15.12-25.25.8-.47 3.95-2.25 4.7-2.68a76.66 76.66 0 0 0 5.38-3.38zm.56.82a77.63 77.63 0 0 1-5.44 3.43l-4.7 2.67c-8.23 4.82-11.57 9.81-14.65 24.6-5.3 25.45-8.51 33.7-18.1 45.21-2.66 3.19-5.68 6.16-10.8 10.84-8.36 7.64-9.24 8.48-11.82 11.42-3.5 4.01-5.27 7.36-5.27 10.42 0 3.18.68 5.1 3.8 12.12 4.27 9.6 5.24 15.37 2.16 23.07-4.18 10.47-6.29 17.78-6.22 23.93.08 8.06 4.26 12.38 13.76 12.38 17.67 0 43.68-9.9 64.75-21.93 17.28-9.88 56.1-41.2 64.84-53.85 14.08-20.42 28.57-34.59 53.17-48.16 13.12-7.23 21.09-14.87 25.17-23.03 2.92-5.86 3.57-10.35 3.57-18.53 0-11.13-.74-18.73-3.7-27.43-3.15-9.22-8.36-16.75-16.09-22.16-21.13-14.8-29.7-16.42-48.5-7.95.7-.32-16.96 7.56-21.17 9.5-1.7.8-3.3 1.55-4.86 2.3a319.68 319.68 0 0 0-22.93 12.17 165.3 165.3 0 0 1-17.85 9.01l-4.37 1.88c-1.04.45-1.92.84-2.76 1.23a74.56 74.56 0 0 0-11.99 6.86zm-7.6 12.2c7.7-6.25 12.3-8.17 23.68-11.27 6.12-1.67 9.12-2.95 12.31-5.72 3.8-3.3 7.47-4.52 15.86-6.1 2.75-.52 3.67-.7 5.06-1.02 5.48-1.24 9.48-2.93 13.1-5.89 10.42-8.53 25.4-14.11 36.31-14.11 5.33 0 16.77 7.58 25.74 17.16 10.73 11.46 15.96 23.27 12.73 32.5-3.18 9.1-11.39 18.57-23.03 27.86-8.44 6.73-18.36 13-25.22 16.43-3.72 1.86-6.59 4.88-9.77 9.99-.69 1.1-11.1 20.25-16.03 27.83-5.62 8.65-15.4 17.36-30.23 27.96a552.58 552.58 0 0 1-9.2 6.42c-.13.09-6.81 4.65-8.6 5.89-6.47 4.46-10.35 7.35-13.05 9.83-11.64 10.67-37.14 15.54-43.7 8.98-1.96-1.96-2.2-4.06-1.95-10.52.37-9.42-.5-14.5-4.95-20.51a34.09 34.09 0 0 0-7.04-6.92c-3.93-2.95-6.07-6.11-6.56-9.49-.97-6.61 3.87-13.06 14.17-21.69 1.58-1.32 6.67-5.44 7.09-5.78a48.03 48.03 0 0 0 5.23-4.77c4.1-4.63 5.85-9.55 7.8-20.07a501.52 501.52 0 0 0 .8-4.37c.33-1.87.6-3.3.88-4.73.74-3.78 1.5-7.18 2.4-10.63 1-3.78 1.38-5.5 2.36-10.37.6-3.02.93-4.21 1.56-5.47 1.22-2.45 1.27-2.5 12.25-11.42zm.64.78c-10.77 8.74-10.88 8.84-12 11.08-.58 1.16-.88 2.3-1.47 5.22-.98 4.89-1.36 6.63-2.37 10.44-.9 3.43-1.65 6.8-2.39 10.56a339.79 339.79 0 0 0-1.29 6.95l-.39 2.15c-1.98 10.68-3.77 15.74-8.04 20.54a48.77 48.77 0 0 1-5.34 4.88c-.42.34-5.5 4.47-7.07 5.78-10.04 8.4-14.72 14.65-13.83 20.78.45 3.1 2.44 6.03 6.17 8.83 3 2.25 5.39 4.62 7.24 7.12 4.63 6.24 5.52 11.52 5.15 21.15-.25 6.14-.01 8.1 1.66 9.78 6.1 6.1 31.02 1.33 42.31-9.02 2.75-2.52 6.66-5.43 13.16-9.92l8.6-5.89c3.63-2.48 6.45-4.44 9.19-6.4 14.73-10.54 24.44-19.18 29.97-27.7 4.9-7.54 15.31-26.68 16.02-27.8 3.27-5.26 6.26-8.41 10.18-10.37 6.79-3.4 16.65-9.63 25.03-16.32 11.52-9.18 19.61-18.53 22.72-27.4 3.07-8.78-2.02-20.27-12.52-31.49-8.8-9.4-20.04-16.84-25.01-16.84-10.67 0-25.43 5.5-35.68 13.89-3.76 3.07-7.9 4.81-13.5 6.09-1.41.32-2.35.5-5.11 1.02-8.21 1.55-11.76 2.73-15.38 5.88-3.34 2.9-6.45 4.22-12.7 5.92-11.26 3.07-15.75 4.94-23.31 11.09zM212 251.85c0 7.56-.6 10.92-2.6 14.3-1.1 1.84-7.66 10.05-8.6 11.3-5.96 7.94-9.33 10.28-17.26 13.76-1.34.58-2.2 1-3.03 1.5-.55.33-1.2.66-2 1.02-.71.33-4.46 1.9-5.52 2.39-6.05 2.78-8.99 5.8-8.99 10.73 0 10.97-18.95 36.12-34.51 44.87-8.18 4.6-21.3 9.36-32.78 11.86-13.33 2.9-22.49 2.48-24.62-2.32-1.32-2.97-4.4-4.26-11.98-5.81l-.6-.12c-4.84-.99-6.94-1.55-9.03-2.64-2.92-1.5-4.48-3.7-4.48-6.84 0-2.74 1.08-5.77 3.25-9.67.85-1.53 1.82-3.13 3.23-5.35-.16.25 2.83-4.4 3.67-5.76 6.69-10.7 9.85-18.5 9.85-27.22 0-18.41 11.22-33.37 27.5-42.86 5.22-3.05 9.23-3.31 15.2-2.12 5.04 1 6.05.9 7.43-1.52 4.5-7.85 7.04-9.5 15.87-9.5 3.93 0 6.97-.98 10.47-3.16 1.56-.97 8.67-6.17 10.99-7.68 9.2-5.98 11.34-7 25.2-11.95 6.95-2.48 15.18 1.28 22.33 9.12 6.55 7.19 11.01 16.61 11.01 23.67zm-2 0c0-6.5-4.25-15.48-10.49-22.32-6.67-7.32-14.16-10.74-20.17-8.59-13.73 4.9-15.73 5.85-24.8 11.75-2.24 1.46-9.37 6.68-11.01 7.7-3.8 2.36-7.2 3.46-11.53 3.46-8.08 0-9.98 1.23-14.13 8.5-1.1 1.91-2.51 2.88-4.35 3.09-1.3.14-1.9.05-5.22-.61-5.53-1.1-9.07-.88-13.8 1.88-15.72 9.17-26.5 23.55-26.5 41.14 0 9.2-3.28 17.29-10.15 28.28l-3.68 5.77c-1.39 2.19-2.35 3.77-3.17 5.25-2.02 3.63-3 6.38-3 8.7 0 4.19 2.87 5.67 11.9 7.52l.61.12c8.27 1.7 11.7 3.13 13.4 6.95 3.17 7.14 36 0 54.6-10.46 14.98-8.43 33.49-32.99 33.49-43.13 0-5.9 3.47-9.48 10.16-12.55 1.1-.5 4.85-2.08 5.52-2.38.74-.34 1.32-.64 1.8-.93.92-.55 1.85-1 3.25-1.62 7.65-3.35 10.75-5.5 16.47-13.12 1.02-1.36 7.47-9.42 8.47-11.11 1.79-3.01 2.33-6.06 2.33-13.3zm-37.18-22.4c.15-.1 2.4-1.51 2.95-1.84.96-.57 1.7-.94 2.43-1.17 2.57-.83 5.06-.1 11.04 3.12 14.86 8 19.43 22.87 9.18 38.71-4.04 6.24-9.37 9-18.72 11.11-.85.2-1.2.27-3.13.68-6.04 1.29-8.78 2.08-11.6 3.65-3.63 2.02-6.09 4.98-7.5 9.44-7.87 24.93-19.72 43.34-36.28 50.31-16.45 6.93-21.13 8.53-27.98 8.89-4.94.25-9.8-.65-15.4-2.89a44.45 44.45 0 0 1-5.64-2.6c-4.02-2.33-5.14-4.74-4.5-9.31.3-2.13 3.77-15.53 4.84-20.65.63-3.05 1.19-6.14 1.75-9.69a464.04 464.04 0 0 0 1.35-8.9c1.42-9.41 2.5-14.27 4.49-18.65 2.46-5.43 6.13-9.03 11.72-11.13 6.59-2.47 10.54-3.1 18.03-3.53 4.75-.27 6.68-.64 9-2.05.61-.37 1.22-.81 1.82-1.33a30.61 30.61 0 0 0 3.37-3.4c.59-.69 2.38-2.9 2.63-3.19 3.36-4 6.3-5.53 12.33-5.53 3.94 0 5.9-.92 8.18-3.36-.17.18 2.75-3.14 3.85-4.22a30.95 30.95 0 0 1 6.79-5c1.5-.83 3.15-1.62 4.99-2.38a64.92 64.92 0 0 0 10.01-5.1zm-14.52 8.34a29.95 29.95 0 0 0-6.57 4.84 116.68 116.68 0 0 0-3.82 4.2c-2.46 2.63-4.68 3.67-8.91 3.67-5.72 0-8.39 1.39-11.57 5.17-.23.28-2.03 2.5-2.63 3.2a31.6 31.6 0 0 1-3.47 3.51c-.65.55-1.3 1.03-1.96 1.43-2.5 1.51-4.55 1.9-9.47 2.19-7.39.42-11.25 1.04-17.72 3.47-5.34 2-8.82 5.4-11.17 10.6-1.93 4.27-3 9.07-4.41 18.39l-.65 4.34-.7 4.57c-.57 3.56-1.12 6.67-1.76 9.73-1.08 5.18-4.54 18.53-4.83 20.59-.59 4.17.35 6.18 4.01 8.3 1.35.77 3.1 1.58 5.52 2.55 5.46 2.18 10.18 3.05 14.97 2.8 6.69-.34 11.32-1.93 27.65-8.8 16.21-6.83 27.92-25.01 35.71-49.7 1.49-4.7 4.12-7.86 7.97-10 2.93-1.63 5.74-2.45 11.87-3.76 1.92-.4 2.28-.49 3.12-.68 9.12-2.06 14.24-4.7 18.1-10.67 9.92-15.34 5.55-29.55-8.82-37.29-5.75-3.1-8.03-3.76-10.25-3.05-.65.2-1.33.54-2.23 1.08-.55.32-2.77 1.72-2.93 1.82a65.91 65.91 0 0 1-10.16 5.17c-1.8.75-3.42 1.52-4.89 2.33zm-42.39 32.72c16.15-2.87 26.36-.97 32.47 6.16 5.08 5.93 1.13 21.42-5.93 35.55-4.79 9.58-10.6 16.21-23.16 25.19-14.15 10.1-35.5 12.2-40.71 3.85-1.86-2.97-2.1-8.14-1.06-15.73.78-5.68 1.86-10.71 4.73-22.98l.12-.51c1.59-6.8 2.37-10.31 3.14-14.14 1.45-7.25 3.74-11.47 7.26-13.74 2.81-1.8 5.53-2.28 12.33-2.62 5.33-.27 7.56-.46 10.81-1.03zm.18.98c-3.3.59-5.56.78-10.94 1.05-6.62.33-9.23.78-11.84 2.46-3.25 2.1-5.42 6.09-6.82 13.1-.77 3.84-1.56 7.35-3.15 14.17l-.12.5c-2.86 12.24-3.93 17.26-4.7 22.9-1.03 7.36-.79 12.36.9 15.07 4.82 7.7 25.54 5.67 39.29-4.15 12.43-8.88 18.13-15.39 22.84-24.81 6.86-13.72 10.75-29 6.07-34.45-5.84-6.81-15.7-8.65-31.53-5.84zM132 276.5c7.12 0 10.66 3.08 11.25 8.7.42 4.02-.43 8.14-2.77 15.94-2.56 8.52-18.36 25.38-27.2 31.28-7.01 4.67-20.02 5.67-26.57.99-3.99-2.85-3.53-12.08.02-26.46.68-2.75 1.47-5.65 2.37-8.76a412.6 412.6 0 0 1 3.05-10.14l.37-1.2c1.48-4.8 5.1-7.75 10.73-9.27 4.4-1.2 9.54-1.5 17.48-1.33l3.89.1c3.87.11 5.42.15 7.38.15zm0 1c-1.97 0-3.53-.04-7.41-.15l-3.88-.1c-7.85-.17-12.92.13-17.2 1.3-5.32 1.43-8.67 4.16-10.03 8.6a1277.83 1277.83 0 0 1-1.6 5.21c-.68 2.2-1.27 4.17-1.82 6.1-.9 3.1-1.68 5.99-2.36 8.73-3.43 13.88-3.87 22.93-.4 25.4 6.17 4.42 18.73 3.45 25.42-1 8.66-5.78 24.33-22.49 26.8-30.73 2.3-7.67 3.14-11.71 2.73-15.56-.53-5.1-3.64-7.8-10.25-7.8zm-17.79 7a31.3 31.3 0 0 1 8.57 1.4c5.42 1.78 8.72 5.03 8.72 10.1 0 9.59-9.51 17.2-22.34 21.47-9.82 3.28-13.62-1.79-11.66-16.54.84-6.28 3.82-10.67 8.24-13.46a20.38 20.38 0 0 1 8.47-2.97zm-.6 1.08a19.39 19.39 0 0 0-7.34 2.73c-4.18 2.64-6.98 6.78-7.77 12.76-1.89 14.11 1.36 18.45 10.34 15.46C121.3 312.37 130.5 305 130.5 296c0-4.56-2.98-7.5-8.03-9.15a28.05 28.05 0 0 0-8.2-1.35c-.13 0-.35.03-.66.08zm80.87-23.45c-2.72 9.8-14.93 9.86-26.72 3.3-10.17-5.64-13.8-17.98-5-22.87a66.53 66.53 0 0 0 4.48-2.7l2.03-1.3a50.15 50.15 0 0 1 3.92-2.3c4.73-2.43 8.82-2.8 14-.72 9.16 3.66 10.98 13.33 7.3 26.6zm-20.83-24.98a49.26 49.26 0 0 0-3.84 2.25l-2.03 1.3c-.84.53-1.5.95-2.16 1.35-.82.5-1.6.96-2.38 1.39-7.94 4.4-4.59 15.8 5 21.12 11.31 6.29 22.8 6.23 25.28-2.7 3.57-12.83 1.85-21.97-6.7-25.4-4.9-1.95-8.69-1.62-13.17.7zm17.85 12.15c0 5.7-2.44 9-6.64 9.96-3.3.76-7.56-.05-11.08-1.81l-1.89-.94c-.67-.34-1.18-.62-1.63-.88-4.07-2.38-4.13-4.97.34-10.93 6.8-9.06 20.9-7.16 20.9 4.6zm-1 0c0-5.3-2.87-8.55-7.32-9.16-4.23-.57-8.99 1.44-11.78 5.16-4.15 5.54-4.1 7.44-.64 9.47.44.25.93.51 1.59.85l1.87.93c3.34 1.67 7.36 2.44 10.42 1.74 3.73-.86 5.86-3.74 5.86-9zM387 530.3c0-12.8 2.44-16.74 18.48-29.77a56.8 56.8 0 0 1 7.61-5.2c2.6-1.5 5.33-2.82 8.5-4.18 1.24-.53 2.48-1.05 4.1-1.7l3.92-1.57c9.4-3.83 13.74-6.7 16.62-12.05 1.2-2.22 2.21-4.4 3.23-6.83a148.57 148.57 0 0 0 1.54-3.84l.3-.74.56-1.44c3.2-8.02 6.05-12.08 12.7-16.5a35.26 35.26 0 0 0 4.96-4 46.36 46.36 0 0 0 3.88-4.29c.27-.34 2.55-3.2 3.2-3.98 3.48-4.15 6.51-5.9 11.51-5.9 3.08 0 5.62-.63 9.57-2.1 5.42-2.02 6.53-2.34 8.96-2.2 2.53.13 4.85 1.26 7.18 3.59 1.3 1.3 5.55 5.83 6.52 6.78 5.06 5 9.44 6.92 17.77 6.92a197.5 197.5 0 0 1 12.08.45c15.93.87 21.94.57 25.28-2.21 6.91-5.77 11.64-2.73 11.64 7.76 0 10.73-8.6 20-19 20-4.8 0-8.32 1.43-9.34 3.67-1.12 2.48.68 6.15 5.98 10.57 13.6 11.33 11.24 20.76-7.64 20.76a21.91 21.91 0 0 0-14.6 5.24c-3.28 2.71-5.8 5.86-9.85 11.82l-1.52 2.25c-3.1 4.57-5.01 7.1-7.32 9.4-6.21 6.21-9.3 7.64-13.05 6.89l-1-.23a10.82 10.82 0 0 0-2.66-.37c-1.6 0-2.41.67-8.18 6.22-4.85 4.67-8.07 6.78-11.82 6.78-1.33 0-3.46 1.15-6.45 3.45-1.27.98-2.68 2.14-4.5 3.7l-4.92 4.29a181.11 181.11 0 0 1-4.54 3.82c-9.33 7.56-15.63 10.2-20.21 6.52-2.7-2.15-4.14-4.51-4.63-7.26-.37-2.04-.26-3.63.29-7.3.87-5.85.65-8.42-1.83-11.6-2.32-2.98-2.96-3.22-3.77-2.39-.25.26-1.35 1.63-1.61 1.94-2.21 2.5-4.85 3.57-9 2.82-4.6-.84-5.57-4.11-4.72-10.09l.24-1.56c.6-3.66.68-4.93.25-5.8-.44-.86-1.9-.94-5.23.4l-.74.29c-13.78 5.54-15.26 6.09-19.43 6.67-6.03.84-9.31-1.6-9.31-7.9zm2 0c0 5 2.14 6.6 7.04 5.92 3.91-.55 5.43-1.1 18.95-6.55l.75-.3c4.17-1.66 6.7-1.54 7.76.58.71 1.43.62 2.76-.06 7l-.24 1.53c-.72 5.04-.06 7.27 3.09 7.84 3.43.62 5.38-.17 7.15-2.18.2-.23 1.34-1.66 1.68-2 1.9-1.96 3.82-1.25 6.78 2.55 2.9 3.74 3.17 6.77 2.22 13.12-1 6.75-.52 9.4 3.62 12.71 3.49 2.8 9.1.45 17.7-6.51 1.35-1.1 2.75-2.28 4.49-3.78l4.93-4.3c1.84-1.58 3.27-2.76 4.58-3.77 3.34-2.56 5.74-3.86 7.67-3.86 3.04 0 5.95-1.9 10.43-6.22l2.46-2.39c.94-.89 1.67-1.56 2.37-2.13 1.81-1.49 3.3-2.26 4.74-2.26 1.03 0 1.81.13 3.1.42.7.16.71.17.96.21 2.96.6 5.45-.55 11.23-6.33 2.2-2.2 4.06-4.65 7.09-9.11l1.52-2.25c4.15-6.11 6.76-9.37 10.22-12.24a23.9 23.9 0 0 1 15.88-5.7c16.87 0 18.62-7.01 6.36-17.23-5.9-4.92-8.12-9.41-6.52-12.93 1.42-3.12 5.67-4.84 11.16-4.84 9.25 0 17-8.34 17-18 0-8.94-2.88-10.79-8.36-6.23-3.94 3.28-9.98 3.59-26.67 2.68l-1.02-.06c-5.09-.27-7.99-.39-10.95-.39-8.88 0-13.76-2.14-19.18-7.5-1-.98-5.26-5.53-6.53-6.79-1.99-1.99-3.86-2.9-5.87-3-2.03-.12-3.06.18-8.15 2.07-4.15 1.55-6.9 2.22-10.27 2.22-4.33 0-6.84 1.46-9.98 5.2-.63.74-2.89 3.6-3.18 3.95a48.29 48.29 0 0 1-4.04 4.46 37.26 37.26 0 0 1-5.24 4.23c-6.26 4.17-8.9 7.91-11.95 15.58l-.57 1.43-.28.74a531.5 531.5 0 0 1-1.56 3.88 77.49 77.49 0 0 1-3.32 7c-3.16 5.88-7.82 8.97-17.63 12.96l-3.92 1.58c-1.6.64-2.84 1.15-4.05 1.67a79.2 79.2 0 0 0-8.3 4.08 54.8 54.8 0 0 0-7.35 5.02C391.12 514.78 389 518.21 389 530.31zm133.22-79.76c3.06 1.53 6.54 2.02 10.68 1.7 2.53-.2 4.91-.62 8.8-1.49 5.36-1.19 6.33-1.38 8.33-1.54 2.78-.23 4.82.17 6.29 1.4 1.58 1.31 1.96 2.72 1.26 4.22-.66 1.38-1.05 1.74-5.05 5.07-3.53 2.93-5.03 4.83-5.03 7.09 0 7.3 1.29 10.02 7.83 15.62 3.86 3.3 5.93 6.84 5.28 9.62-.75 3.25-4.96 5.02-12.61 5.02-7.18 0-12.7 4.61-20.03 14.68-.5.7-3.96 5.57-4.94 6.87a38.89 38.89 0 0 1-4.72 5.5c-1.06.98-2.09 1.7-3.1 2.15-2.85 1.26-5.05 1.57-9.83 1.74-7.66.27-10.87 1.45-14.98 7.1-1.58 2.17-3.11 4-4.68 5.6a42.87 42.87 0 0 1-8.65 6.69c-.15.08-10.69 6.19-14.8 8.83-3.76 2.42-6.45 2.04-8.22-.77-1.28-2.03-1.9-4.54-2.87-10.35-.84-5.08-1.27-7.08-2.06-8.93-.97-2.3-2.21-3.24-4.02-2.88-6.2 1.24-8.95 1.39-10.98.2-2.37-1.4-3.13-4.62-2.62-10.73.16-1.96-1.04-2.87-3.76-3.04-2.24-.13-4.9.2-9.94 1.12l-.69.12c-7.97 1.45-10.72 1.72-12.72.73-2.91-1.43-1.6-5.27 4.23-12.21 5.48-6.53 10.6-10.81 15.76-13.53 3.74-1.97 5.94-2.65 12.16-4.1 7.29-1.72 10.4-3.51 14.04-9.31 2.96-4.75 10.74-18.62 12.14-20.84 3.59-5.67 6.8-9.1 11.05-11.34 2.6-1.38 4.72-2.82 9.17-6.07l1.38-1.01c7.85-5.72 12.3-7.98 17.68-7.98 4.22 0 6.49 1.36 9.13 4.77.34.43 1.67 2.22 2 2.67.85 1.09 1.6 1.98 2.45 2.83a24.29 24.29 0 0 0 6.64 4.78zm-.44.9c-2.8-1.4-5-3.03-6.92-4.97-.87-.9-1.65-1.81-2.51-2.93-.35-.46-1.68-2.25-2.01-2.67-2.47-3.18-4.46-4.38-8.34-4.38-5.09 0-9.4 2.2-17.09 7.78l-1.38 1.01c-4.49 3.29-6.63 4.74-9.3 6.15-4.06 2.15-7.16 5.45-10.66 11-1.39 2.19-9.16 16.05-12.15 20.82-3.79 6.07-7.13 7.98-14.66 9.75-6.13 1.45-8.27 2.1-11.92 4.02-5.04 2.66-10.05 6.86-15.46 13.3-5.43 6.46-6.53 9.69-4.55 10.66 1.7.84 4.48.57 12.1-.81l.7-.13c5.12-.93 7.82-1.27 10.17-1.12 3.21.2 4.92 1.48 4.7 4.11-.48 5.76.2 8.64 2.13 9.78 1.73 1.02 4.34.88 10.27-.31 2.35-.47 4 .78 5.14 3.47.83 1.95 1.27 4 2.07 8.8l.06.36c.94 5.65 1.55 8.11 2.72 9.98 1.46 2.3 3.52 2.6 6.84.46 4.14-2.66 14.69-8.77 14.81-8.85a41.9 41.9 0 0 0 8.46-6.54 47.89 47.89 0 0 0 4.6-5.48c4.32-5.95 7.81-7.23 15.74-7.5 4.66-.17 6.76-.47 9.46-1.67.9-.4 1.85-1.06 2.84-1.96a38.03 38.03 0 0 0 4.6-5.36c.96-1.3 4.4-6.16 4.93-6.87 7.5-10.31 13.22-15.09 20.83-15.09 7.24 0 11.02-1.6 11.64-4.24.54-2.32-1.36-5.55-4.97-8.64-6.75-5.79-8.17-8.79-8.17-16.38 0-2.67 1.64-4.74 5.39-7.86 3.8-3.17 4.23-3.56 4.78-4.73.5-1.06.25-1.99-.99-3.03-2.23-1.85-4.72-1.65-13.76.36-3.93.87-6.35 1.3-8.94 1.5-4.3.34-7.97-.18-11.2-1.8zm-28-3.9c5.65-2.82 8.96-2.2 12.9 1.37.56.5 2.6 2.47 3.02 2.87 4.2 3.89 8.07 5.71 14.3 5.71 11.37 0 14 1.41 16.1 8.09.26.83 1.35 4.6 1.66 5.62.8 2.63 1.64 5.03 2.7 7.6 2.13 5.17 2.64 8.32 1.72 10.24-.77 1.61-2.1 2.18-5.37 2.79-2.32.43-2.8.53-3.85.85-1.85.58-3.35 1.4-4.6 2.66-1 1-2.02 2.13-3.31 3.66-.6.71-2.91 3.5-3.46 4.14-7.2 8.54-12.43 12.35-19.59 12.35-3.76 0-6.95 1.28-10.59 4-1.84 1.37-11.62 10.31-15.22 13.06a73.09 73.09 0 0 1-8.95 5.88c-4.58 2.54-7.35 3.22-8.98 2.23-1.32-.8-1.65-2.07-1.94-5.5a52.53 52.53 0 0 0-.16-1.81c-.54-4.73-2.24-6.86-7.16-6.86-7.11 0-8.85-1.23-9.73-5.41-.96-4.61-2.1-6.7-6.55-9.67-3.97-2.65-4.31-5.42-1.52-8.22 2-2 4.63-3.5 11.35-6.87 6.61-3.3 9.2-4.8 11.1-6.68a39.09 39.09 0 0 0 5.3-6.48c.98-1.5 1.83-3.04 2.88-5.13l2.12-4.3c.91-1.83 1.72-3.37 2.61-4.98 5.74-10.32 10.37-14.78 23.22-21.2zm-22.34 21.7c-.89 1.59-1.69 3.12-2.6 4.94l-2.11 4.3a52.9 52.9 0 0 1-2.94 5.23 40.08 40.08 0 0 1-5.44 6.63c-2 2-4.62 3.51-11.35 6.87-6.6 3.3-9.2 4.8-11.1 6.69-2.33 2.34-2.08 4.37 1.38 6.67 4.7 3.14 5.96 5.46 6.97 10.3.78 3.7 2.09 4.62 8.75 4.62 5.5 0 7.57 2.57 8.15 7.75.06.5.09.82.17 1.84.25 3.06.55 4.17 1.46 4.72 1.2.74 3.69.13 7.98-2.25a72.09 72.09 0 0 0 8.82-5.8c3.55-2.7 13.34-11.65 15.24-13.07 3.79-2.83 7.18-4.19 11.18-4.19 6.77 0 11.8-3.67 18.83-12l3.45-4.13a60.07 60.07 0 0 1 3.37-3.72 11.72 11.72 0 0 1 5.01-2.91c1.1-.34 1.6-.45 3.97-.89 2.95-.55 4.07-1.02 4.65-2.23.76-1.59.28-4.5-1.74-9.43a84.46 84.46 0 0 1-2.74-7.69c-.31-1.03-1.4-4.8-1.66-5.61-1.95-6.2-4.16-7.39-15.14-7.39-6.5 0-10.61-1.93-14.98-5.98-.44-.4-2.46-2.37-3.01-2.86-3.65-3.3-6.52-3.85-11.79-1.21-12.67 6.33-17.15 10.65-22.78 20.8zm55.86 11.93c-2.98 6.45-16.78 15.26-26.74 15.26-5.33 0-7.56-2.98-7.11-7.86.32-3.48 2.1-7.91 3.93-10.61l1.52-2.32a44.95 44.95 0 0 1 1.88-2.7c3.66-4.8 7.85-7.45 13.62-7.45 9.06 0 15.75 9.52 12.9 15.68zm-.9-.42c2.52-5.47-3.65-14.26-12-14.26-5.4 0-9.33 2.48-12.82 7.06-.6.8-1.17 1.6-1.85 2.64 0 0-1.2 1.87-1.52 2.33-1.74 2.57-3.46 6.85-3.77 10.14-.4 4.33 1.43 6.77 6.12 6.77 9.57 0 23.02-8.58 25.83-14.68zm-69.67 20.74c2.08.18 4.44.81 5.88 1.8 2.12 1.47 2.2 3.6-.26 6.05-5.14 5.15-12.85 4.34-12.85-1.35 0-4.66 3.14-6.84 7.23-6.5zm-.09 1c-3.56-.3-6.14 1.5-6.14 5.5 0 4.58 6.53 5.26 11.15.65 2.03-2.04 1.98-3.43.4-4.52-1.27-.88-3.48-1.47-5.4-1.63zm29.59-225.95c4.64 2.35 17.27 8.24 19.39 9.43a24.14 24.14 0 0 1 7.05 5.64 45.03 45.03 0 0 1 3.75 5.2c2.4 3.78.04 7.66-6.2 11.63-4.97 3.16-12.18 6.3-21.95 9.82-4.84 1.74-19.63 6.68-21.1 7.2-6.59 2.33-14.85.1-25.14-5.86-3.93-2.27-8-5-12.94-8.54-2.23-1.61-9.5-6.99-10.7-7.85a81.21 81.21 0 0 0-8.63-5.7c-4.82-2.6-4.45-6.64.17-12.13 3.27-3.88 4.17-4.67 18.1-16.33a230.2 230.2 0 0 0 8.89-7.74 95.2 95.2 0 0 0 4.72-4.66c5.08-5.43 9.8-6.49 14.97-3.92 2.24 1.1 4.53 2.85 7.43 5.52 1.48 1.37 6.94 6.72 7.98 7.7 5.2 4.91 9.46 8.2 14.2 10.6zm-.46.9c-4.85-2.45-9.18-5.79-14.44-10.76-1.05-1-6.5-6.34-7.97-7.69-2.83-2.61-5.06-4.3-7.2-5.37-4.75-2.36-9-1.4-13.8 3.71a96.18 96.18 0 0 1-4.76 4.71c-2.48 2.3-5.16 4.62-8.92 7.77-13.86 11.6-14.77 12.4-17.98 16.21-4.28 5.08-4.58 8.4-.46 10.61 2.23 1.2 4.9 2.99 8.74 5.77 1.2.87 8.47 6.24 10.7 7.85a154.8 154.8 0 0 0 12.85 8.49c10.06 5.82 18.07 7.98 24.3 5.78 1.48-.52 16.27-5.47 21.1-7.2 9.7-3.5 16.86-6.61 21.75-9.72 5.84-3.71 7.9-7.1 5.9-10.26a44.09 44.09 0 0 0-3.67-5.08 23.16 23.16 0 0 0-6.78-5.42c-2.08-1.16-14.68-7.05-19.36-9.4zm-38.83 8.05c3.11-.37 5.7-.13 8.4.7 2.15.66 2.74.93 8.64 3.77 4.75 2.29 8.39 3.86 13.19 5.56 8.38 2.97 11.32 6.23 8.83 9.76-2.08 2.94-8.04 5.92-17.84 9.18-8.45 2.82-15.48 2.35-21.43-.9-4.65-2.55-8.33-6.5-12.15-12.3-2.9-4.41-2.73-8.2.16-11.06 2.48-2.45 6.87-4.07 12.2-4.7zm.12 1c-5.13.6-9.33 2.16-11.62 4.42-2.53 2.5-2.68 5.77-.02 9.8 3.73 5.68 7.3 9.51 11.8 11.97 5.7 3.11 12.43 3.57 20.62.84 9.59-3.2 15.44-6.12 17.34-8.82 1.94-2.75-.5-5.45-8.35-8.24-4.84-1.72-8.5-3.3-13.28-5.6-5.84-2.81-6.42-3.07-8.5-3.71a18.42 18.42 0 0 0-8-.66zM202.5 500.38c0 4.78-1.45 7.56-4.43 8.93-2.29 1.05-4.55 1.23-10.79 1.2l-1.78-.01c-9.19 0-17-7.65-17-15.5 0-7.59 10.6-10.51 19.74-5.44 2.78 1.55 4.21 1.94 8.57 2.75 4.44.83 5.69 2.27 5.69 8.07zm-1 0c0-5.3-.9-6.34-4.88-7.08-4.45-.83-5.96-1.25-8.86-2.86-8.57-4.76-18.26-2.1-18.26 4.56 0 7.3 7.36 14.5 16 14.5h1.79c6.06.04 8.26-.14 10.36-1.1 2.6-1.2 3.85-3.6 3.85-8.02zm33.33-117.85c3.71-1.31 8.7-2.7 16.1-4.55 2.58-.65 16.53-4.04 20.56-5.05 19.59-4.93 31.55-8.9 38.23-13.35 14.93-9.95 36.87-33.88 43.83-47.8 2.25-4.5 4.65-6.38 7.68-6.25 1.26.06 2.61.45 4.32 1.2a50.81 50.81 0 0 1 3.54 1.7l1.26.63c4.78 2.34 8.38 3.44 12.65 3.44 7.2 0 10.01 3.07 8.35 7.91-1.4 4.06-5.92 8.91-11.1 12.02-8.3 4.98-11.75 17.3-11.75 33.57 0 3.59-1.37 6.28-3.98 8.36-1.98 1.58-4.2 2.6-8.47 4.16l-1.02.37c-4.85 1.75-6.98 2.77-8.68 4.46-5.09 5.1-12.54 7.15-20.35 7.15-1.38 0-2.47.92-3.99 3.1-.29.41-1.32 1.95-1.47 2.18-2.68 3.92-4.93 5.72-8.54 5.72-7.84 0-10.74.93-21.76 6.94-5.18 2.82-8.8 3.58-14.66 3.68-.26 0-.47 0-.92.02-4.82.06-7.12.3-10.51 1.34a73.43 73.43 0 0 0-8.89 3.56c-2.17 1-10.53 5.01-10.23 4.87-7.79 3.7-13.32 5.98-18.9 7.57-12.41 3.55-18.58 2.24-27.42-4.07-2.58-1.85-2.72-4.43-.83-7.62 1.45-2.45 3.9-5.09 8.08-8.97l1.78-1.64c3.92-3.6 4.48-4.11 5.9-5.53 2.32-2.32 3.12-3.5 5.48-7.63 1.93-3.36 3.37-5.11 6.27-7.06 2.3-1.54 5.34-2.98 9.44-4.43zm.34.94c-4.03 1.42-7 2.83-9.22 4.32-2.75 1.85-4.1 3.49-5.96 6.73-2.4 4.2-3.24 5.44-5.64 7.83-1.43 1.44-2 1.96-5.94 5.57l-1.77 1.63c-4.1 3.82-6.52 6.41-7.9 8.75-1.65 2.79-1.54 4.8.55 6.3 8.6 6.14 14.46 7.38 26.57 3.92 5.5-1.57 11-3.84 18.74-7.51-.3.14 8.06-3.88 10.24-4.88a74.3 74.3 0 0 1 9.01-3.6c3.51-1.09 5.89-1.33 10.8-1.4h.91c5.72-.1 9.18-.83 14.2-3.57 11.16-6.08 14.2-7.06 22.24-7.06 3.19 0 5.2-1.6 7.71-5.28l1.48-2.2c1.7-2.43 3-3.52 4.81-3.52 7.57 0 14.78-2 19.65-6.85 1.83-1.84 4.04-2.9 9.04-4.7l1.02-.37c8.6-3.13 11.79-5.67 11.79-11.58 0-16.6 3.53-29.2 12.24-34.43 5-3 9.35-7.67 10.66-11.48 1.42-4.13-.83-6.59-7.4-6.59-4.45 0-8.19-1.14-13.09-3.54-7.52-3.67-6.78-3.34-8.72-3.43-2.58-.1-4.65 1.52-6.74 5.7-7.04 14.07-29.1 38.14-44.17 48.19-6.81 4.54-18.84 8.52-38.55 13.48-4.03 1.02-17.98 4.4-20.56 5.05-7.37 1.84-12.33 3.23-16 4.52zM252 387.5c2.08 0 4-.2 7.25-.69 5.22-.77 6.64-.9 8.46-.5 2.52.56 3.79 2.35 3.79 5.69 0 4.05-2.27 7.29-6.62 10.11-3.24 2.1-6.53 3.53-14.15 6.4l-.27.1-2.28.86c-3.04 1.16-5.27 2.52-9.33 5.43l-.8.57c-8.19 5.88-13.35 8.03-23.05 8.03-4.98 0-6.88-2.03-5.75-5.62.87-2.81 3.58-6.56 7.8-11.13 1.26-1.37 2.64-2.8 4.15-4.3 3.17-3.14 11.25-10.61 11.45-10.8.46-.47.93-.89 1.4-1.26 3.38-2.71 5.77-3.08 14.18-2.93 1.65.03 2.63.04 3.77.04zm0 1c-1.15 0-2.13-.01-3.79-.04-8.18-.14-10.4.2-13.54 2.71-.44.35-.88.74-1.32 1.18-.2.21-8.3 7.69-11.45 10.82a134.6 134.6 0 0 0-4.12 4.26c-4.12 4.47-6.76 8.12-7.58 10.75-.9 2.88.45 4.32 4.8 4.32 9.46 0 14.44-2.07 22.46-7.84l.8-.57c4.13-2.96 6.42-4.36 9.56-5.56l2.3-.86.25-.1c7.55-2.84 10.8-4.25 13.97-6.3 4.08-2.65 6.16-5.6 6.16-9.27 0-2.89-.97-4.26-3-4.7-1.65-.37-3.05-.25-8.1.5-3.3.5-5.26.7-7.4.7zm112.47-45.34c-1.88 5.44-1.98 6.76-.98 12.76 1.18 7.06-1.38 16.58-5.49 16.58a16.89 16.89 0 0 0-1.51.07l-.64.04c-2.86.18-4.83.17-6.94-.17-6.55-1.06-10.41-5.14-10.41-13.44 0-13.9 2.14-19.69 8.13-26.33a21.9 21.9 0 0 0 2.52-3.75c.59-1.03 2.78-5.13 2.72-5.01 4.44-8.14 7.71-11.53 12.25-10.4 1.17.3 2.2.77 3.58 1.59l1.39.84a20 20 0 0 0 3.1 1.6c.7.27 1.8.32 4.75.26l.72-.01c3.16-.05 4.78.08 5.83.66 1.61.89 1.2 2.56-1.14 4.9a215.9 215.9 0 0 1-3.86 3.76c-10.6 10.1-12.75 12.4-14.02 16.05zm-.94-.32c1.34-3.9 3.46-6.17 14.27-16.46 1.55-1.47 2.73-2.62 3.85-3.73 1.94-1.95 2.17-2.88 1.35-3.33-.82-.45-2.37-.58-5.32-.53l-.72.01c-3.14.06-4.26.02-5.14-.34-1.06-.41-1.97-.9-3.25-1.67l-1.38-.83a12.1 12.1 0 0 0-3.31-1.47c-3.88-.97-6.92 2.17-11.13 9.9.07-.13-2.14 3.98-2.73 5.02a22.71 22.71 0 0 1-2.65 3.92c-5.81 6.47-7.87 12-7.87 25.67 0 7.79 3.48 11.47 9.57 12.45 2.01.33 3.92.34 6.71.16a371.33 371.33 0 0 0 1.23-.07c.42-.03.73-.04.99-.04 3.2 0 5.6-8.9 4.5-15.42-1.02-6.16-.91-7.64 1.03-13.24zm-9.26 12.42c.58.52 2.5 1.9 2.55 1.93 1.96 1.57 2.04 3.31.01 6.36-3.74 5.64-8.83 3.09-8.83-4.55 0-3.81.51-5.67 2.07-6.02 1.18-.26 2 .3 4.2 2.28zm-1.34 1.48c-1.5-1.35-2.23-1.85-2.43-1.8-.17.03-.5 1.23-.5 4.06 0 5.87 2.67 7.21 5.17 3.45 1.5-2.26 1.47-2.84.4-3.7.03.03-1.95-1.4-2.64-2zm222.9-130.19c2.2-1.1 3.67-1.66 5.88-2.36l.28-.09a48.92 48.92 0 0 0 8.79-3.55c4.17-2.08 6.35-1.88 6.96.84.44 2 .2 4.01-1.25 12.7-2.27 13.62-9.16 26.14-21.17 36.3-4.3 3.63-7.41 4.39-9.75 2.44-1.88-1.57-3.1-4.57-4.61-10.48-.3-1.15-1.43-5.83-1.72-6.96a114.18 114.18 0 0 0-2.71-9.22c-2.4-6.82-3.03-10.78-2.1-12.94.77-1.83 2.08-2.24 5.6-2.45 1.49-.09 2.09-.14 2.97-.28l1.95-.33c.72-.12 1.22-.2 1.68-.29 1.1-.2 1.92-.38 2.71-.6 1.7-.49 3.42-1.2 6.49-2.73zm.44.9c-3.11 1.54-4.88 2.29-6.65 2.79-.84.23-1.69.42-2.81.63a108.77 108.77 0 0 1-3.81.63c-.77.13-1.39.19-2.92.28-3.13.18-4.17.51-4.74 1.85-.78 1.84-.2 5.62 2.13 12.2a115.12 115.12 0 0 1 2.74 9.31l1.72 6.96c1.46 5.7 2.62 8.58 4.28 9.96 1.87 1.56 4.49.93 8.47-2.44 11.82-10 18.6-22.3 20.83-35.7 1.4-8.45 1.65-10.51 1.25-12.31-.41-1.87-1.86-2-5.54-.16a49.87 49.87 0 0 1-8.93 3.6l-.28.1a35.4 35.4 0 0 0-5.74 2.3zm-4.5 6.58c1.37-.32 2.5-.75 3.9-1.42.35-.18 2.57-1.31 3.32-1.67 1.5-.71 2.97-1.31 4.7-1.89 2.7-.9 4.64-.77 5.88.4.98.94 1.34 2.26 1.41 4.18.02.4.02.7.02 1.37 0 5.63-4.63 16.88-11.34 22.75-4.34 3.8-7.31 4.67-9.92 2.52-2.06-1.7-3.5-4.65-6.67-12.91-1.86-4.83-2.05-8.1-.68-10.2 1.12-1.7 2.9-2.36 5.83-2.7l1.26-.12c1.19-.12 1.75-.19 2.3-.31zm-2.1 2.3l-1.22.12c-2.4.27-3.7.76-4.39 1.81-.93 1.43-.78 4.1.87 8.38 3.02 7.84 4.41 10.71 6.08 12.09 1.63 1.34 3.64.75 7.33-2.48C584.6 250.77 589 240.08 589 235c0-.64 0-.93-.02-1.29-.05-1.44-.3-2.33-.79-2.8-.6-.57-1.8-.65-3.87.04a37.95 37.95 0 0 0-4.47 1.8c-.72.34-2.93 1.47-3.32 1.66a19.54 19.54 0 0 1-4.3 1.56c-.66.16-1.28.24-2.56.36zm-227.73-88.98c-1.59 4.3-3.54 7.25-7.14 11.4l-2.6 2.97a67.02 67.02 0 0 0-2.63 3.23 46.4 46.4 0 0 0-4.68 7.5c-2.85 5.7-7.14 10.18-12.85 13.89-4.25 2.76-8.25 4.62-15.67 7.59-11.01 4.4-16.43 1.26-27.22-16.4-2.86-4.69-8.8-8.63-17.98-12.66-3-1.33-12.88-5.24-14.43-5.92-4.96-2.18-7.04-3.72-6.42-5.85.67-2.32 5.3-4.05 15.48-6.08 16.63-3.32 26.93-3.82 39.93-3.02 7.9.49 9.67.5 12.74-.26 1.99-.48 3.92-1.3 6-2.6l2.79-1.71c9.86-6.14 12.94-7.96 17.3-9.9 6.03-2.71 10.57-3.32 13.94-1.4 7.2 4.12 7.68 7.7 3.44 19.22zm-1.88-.7c3.95-10.7 3.6-13.26-2.56-16.78-2.66-1.52-6.62-.99-12.12 1.48-4.24 1.9-7.3 3.7-17.07 9.77l-2.79 1.73a22.6 22.6 0 0 1-6.57 2.84c-3.36.81-5.22.8-13.34.3-12.84-.78-22.97-.29-39.41 3-4.9.97-8.45 1.88-10.79 2.75-2.03.76-3.04 1.45-3.17 1.91-.16.57 1.48 1.79 5.3 3.46 1.5.67 11.39 4.58 14.44 5.93 9.52 4.19 15.74 8.3 18.87 13.44 10.35 16.93 14.87 19.56 24.78 15.6 7.3-2.93 11.21-4.75 15.33-7.42 5.42-3.53 9.47-7.75 12.15-13.1 1.44-2.9 3.02-5.4 4.86-7.82a68.95 68.95 0 0 1 2.72-3.33l2.6-2.97c3.46-3.99 5.28-6.75 6.77-10.79zm-6.64-.39c-7.94 12.8-18.53 21.75-33.3 25.23-7.82 1.83-12.47-.79-13.12-5.93-.55-4.45 2.29-9.06 6-9.06 3.02 0 5.6-1.68 15.38-9.16 1.47-1.12 2.57-1.96 3.66-2.74 4.4-3.2 7.77-5.17 10.82-6.08 5.57-1.67 9.33-2.15 11.35-1.22 2.5 1.14 2.22 4.13-.79 8.96zm-.84-.52c2.72-4.4 2.94-6.74 1.21-7.53-1.71-.79-5.32-.33-10.65 1.27-2.9.87-6.2 2.79-10.51 5.92-1.08.79-2.18 1.62-3.65 2.74-10.08 7.72-12.62 9.36-15.98 9.36-3.02 0-5.5 4.02-5 7.94.56 4.5 4.62 6.78 11.89 5.07 14.48-3.4 24.86-12.18 32.69-24.77zM461.17 33.53c13.88 4.96 20.75 4.96 31.62.01 3.02-1.37 5.47-2.94 11-6.82 5.57-3.92 8.05-5.51 11.14-6.92 4.14-1.88 7.78-2.38 11.22-1.28 3.92 1.26 6.2 12.3 6.78 28.45.5 14.2-.52 28.93-2.46 34.2-1.82 4.93-5.86 8.17-11.51 10.02A41.7 41.7 0 0 1 506 93.01c-5.79 0-9 2.4-12.2 7.64-.37.59-1.55 2.6-1.71 2.87-1.75 2.9-3.05 4.33-4.93 4.95-.94.32-2.07.83-3.87 1.74l-2.43 1.23c-1.03.53-1.87.94-2.7 1.34-6.43 3.1-11.73 4.72-17.16 4.72-5.71 0-10.04 2.09-14.02 5.92-1.16 1.11-4.2 4.53-4.63 4.94-2.54 2.44-5.93 4.24-10.85 6.1-1.4.52-5.98 2.13-6.25 2.22l-2.06.78c-.89.36-1.78.63-2.7.81-5.55 1.14-11.14-.54-17.98-4.42-1.27-.73-5.13-3.06-5.76-3.42-2.05-1.16-4.12-1.53-9.09-1.9l-1.73-.15c-4.78-.4-7.68-1.14-10.22-2.97-5-3.61-6.77-7.76-5.65-12.33 1.33-5.42 6.5-11.02 14.85-17.28a169.2 169.2 0 0 1 6.5-4.61c-.33.23 4.33-2.92 5.3-3.6 2.73-1.91 4.8-3.9 12.75-12.04l1.09-1.1c3.49-3.56 5.89-5.89 8.12-7.83 2.9-2.5 4.72-5.95 7.5-13.05l.63-1.61c2.7-6.92 4.28-10 6.87-12.33 1.42-1.28 6.68-6.54 7.93-7.5 3.98-3 8.01-2.73 19.57 1.4zm-.34.94c-11.26-4.02-15-4.28-18.62-1.53-1.19.9-6.4 6.11-7.88 7.43-2.42 2.18-3.96 5.19-6.6 11.95l-.63 1.61c-2.83 7.26-4.72 10.8-7.77 13.45a141.85 141.85 0 0 0-9.16 8.87c-8.02 8.2-10.08 10.2-12.88 12.16-.99.69-5.65 3.84-5.31 3.6-2.5 1.71-4.52 3.13-6.47 4.59-8.17 6.13-13.23 11.6-14.48 16.72-1.02 4.15.58 7.9 5.26 11.27 2.36 1.7 5.11 2.4 9.72 2.8l1.73.13c5.12.4 7.28.78 9.5 2.05.65.36 4.5 2.7 5.76 3.4 6.66 3.78 12.04 5.4 17.29 4.32.86-.17 1.7-.42 2.52-.75a67 67 0 0 1 2.1-.8c.28-.1 4.86-1.7 6.24-2.22 4.8-1.8 8.08-3.56 10.5-5.88.4-.38 3.44-3.8 4.63-4.94 4.16-4 8.72-6.2 14.72-6.2 5.25 0 10.42-1.59 16.73-4.62.82-.4 1.65-.8 2.68-1.33.12-.06 1.93-.99 2.43-1.23 1.84-.93 3-1.46 4-1.8 1.6-.52 2.76-1.82 4.39-4.52l1.7-2.88c3.39-5.5 6.87-8.11 13.07-8.11 4.45 0 8.73-.49 12.64-1.77 5.4-1.76 9.2-4.8 10.9-9.41 1.87-5.11 2.9-19.75 2.39-33.83-.56-15.53-2.81-26.48-6.08-27.52-3.18-1.02-6.57-.55-10.5 1.23-3.02 1.37-5.47 2.94-11 6.83-5.57 3.92-8.05 5.5-11.14 6.92-11.13 5.05-18.26 5.05-32.38.01zM475 55c5.38 0 7.55-.21 9.72-.96 1.26-.43 9.95-4.8 14.88-6.96 1.9-.82 3.56-2.44 6.6-6.04 2.56-3.04 3.19-3.75 4.4-4.84 3.7-3.35 7.07-3.28 10.22 1.23 6.23 8.9 5.61 15.94.07 27.02a71.26 71.26 0 0 0-2.5 5.48c-.32.8-1 2.7-1.09 2.9-.17.45-.34.81-.54 1.17-.63 1.14-1.56 2.21-4.05 4.7-2.4 2.4-5.16 3.27-11.68 4.33-1.81.3-2.2.36-3 .51-6.02 1.1-9.6 2.69-12.24 6.07-3.57 4.59-7.9 7.48-14.98 10.74-.55.24-1.1.5-1.8.8l-1.78.8a60.08 60.08 0 0 0-7.7 3.9c-2.57 1.6-4.79 2.35-9.42 3.46-8.58 2.06-12.28 3.76-17.37 9.36-5.12 5.64-10.17 7.64-16.63 6.7-5.36-.79-10.63-3.01-23.56-9.48-6.3-3.15-6.43-7.78-1.5-13.56 3.38-3.94 3.52-4.06 19.4-16.44 8.12-6.33 12.97-10.57 16.63-14.88 2.53-2.98 4.2-5.73 4.96-8.3 5.5-18.3 12.5-21.98 22.78-15.56 1.95 1.22 6.61 4.55 7.18 4.9 3.36 2.15 6.52 2.95 13 2.95zm0 2c-6.84 0-10.37-.89-14.08-3.26-.63-.4-5.27-3.71-7.16-4.9-9.05-5.65-14.66-2.7-19.8 14.45-.86 2.87-2.67 5.85-5.35 9.01-3.78 4.45-8.7 8.75-16.94 15.17-15.66 12.21-15.86 12.38-19.1 16.16-4.17 4.9-4.09 8 .88 10.48 12.71 6.35 17.89 8.54 22.94 9.28 5.78.84 10.18-.9 14.87-6.06 5.42-5.96 9.45-7.82 18.38-9.96 4.43-1.07 6.5-1.76 8.83-3.22a61.7 61.7 0 0 1 7.94-4.02l1.78-.8 1.78-.8c6.82-3.13 10.91-5.87 14.24-10.14 3-3.87 7-5.64 13.46-6.82.83-.15 1.21-.21 3.04-.51 6.1-1 8.6-1.78 10.58-3.77 2.36-2.36 3.21-3.34 3.72-4.26.15-.27.29-.56.44-.94.06-.15.75-2.06 1.09-2.9.64-1.6 1.45-3.4 2.57-5.64 5.24-10.49 5.8-16.8.07-24.98-2.4-3.44-4.37-3.48-7.24-.89-1.11 1-1.73 1.7-4.22 4.65-3.24 3.85-5.04 5.59-7.32 6.59-4.82 2.1-13.62 6.53-15.03 7.01-2.44.84-4.79 1.07-10.37 1.07zm-12.7 8.6c5.47 3.9 10.34 3.72 18.23.88 5.39-1.94 5.92-2.1 7.7-2.1 2.5-.01 4.21 1.36 5.24 4.46 1.66 4.98-2.32 8.52-12.3 12.68-2.7 1.13-16.25 6.18-20 7.73-7.86 3.24-13.93 6.42-18.87 10.15-13.02 9.84-18.36 11.93-23.71 9.68a24.67 24.67 0 0 1-3.62-1.98l-1.99-1.28a90.4 90.4 0 0 0-2.24-1.4c-3.33-2-2.82-4.28.85-7.34 1.35-1.13 10.66-7.61 13.53-9.91 7.1-5.69 11.91-11.47 14.41-18.34 3.07-8.45 4.89-12.1 6.8-13.39 1.73-1.16 3.36-.53 6.18 1.9.63.56 3.4 3.08 4.11 3.7 1.93 1.7 3.71 3.15 5.67 4.55zm-.6.8c-1.98-1.42-3.79-2.88-5.74-4.6-.73-.64-3.48-3.16-4.1-3.7-2.5-2.16-3.75-2.65-4.97-1.83-1.66 1.11-3.44 4.7-6.42 12.9-2.57 7.07-7.5 12.99-14.72 18.78-2.91 2.33-12.21 8.8-13.52 9.9-3.22 2.68-3.56 4.17-.97 5.72l2.26 1.4 1.99 1.28c1.47.93 2.48 1.5 3.47 1.91 4.9 2.07 9.96.07 22.72-9.56 5.02-3.79 11.15-7 19.1-10.28 3.76-1.55 17.3-6.6 20-7.72 9.5-3.97 13.14-7.2 11.73-11.44-.9-2.71-2.25-3.8-4.3-3.79-1.6 0-2.15.17-7.36 2.05-8.17 2.94-13.34 3.14-19.16-1.01z'%3E%3C/path%3E%3C/svg%3E"); + } + position: relative; + padding: 24px; + top: ${headerHeight}px; +`; diff --git a/src/components/home/gradient.tsx b/src/components/home/gradient.tsx index 8805d8a4..f3c037b5 100644 --- a/src/components/home/gradient.tsx +++ b/src/components/home/gradient.tsx @@ -1,4 +1,4 @@ -import { css } from "styled-components"; +import styled from "@emotion/styled"; interface IGradient { colorA: string; @@ -6,7 +6,7 @@ interface IGradient { colorC: string; } -export const Gradient = css` +export const Gradient = styled.span` background-color: ${(properties) => properties.colorB}; background-image: linear-gradient( 30deg, diff --git a/src/components/home/home-ui.tsx b/src/components/home/home-ui.tsx index c9903286..e9aa0eab 100644 --- a/src/components/home/home-ui.tsx +++ b/src/components/home/home-ui.tsx @@ -1,15 +1,7 @@ -// import { TextField, IconButton, Grid } from "@material-ui/core"; -import { Link } from "react-router-dom"; -import styled, { createGlobalStyle } from "styled-components"; -import { Gradient } from "./gradient"; +import { Link } from "react-router"; +import styled from "@emotion/styled"; import { headerHeight } from "@styles/constants"; -export const GlobalStyle = createGlobalStyle` - body { - ${Gradient} - } -`; - export const StyledGrid = styled.div` && { background-color: black; @@ -36,27 +28,6 @@ export const StyledTextField = styled.div` } `; -// interface IAnimatedGridContainer { -// duration: number; -// } - -// export const AnimatedGridContainer = styled(Grid)` -// position: relative; -// transition: all ${(properties) => properties.duration}ms; - -// &.entering { -// opacity: 0; -// transform: translate(30px); -// } -// &.entered { -// opacity: 1; -// } -// &.exiting { -// opacity: 0; -// transform: translate(30px); -// } -// `; - interface IProjectCard { duration: number; projectIndex: number; @@ -92,9 +63,7 @@ interface IProjectCardSpinnerContainer { duration: number; } -export const ProjectCardSpinnerContainer = styled.div< - IProjectCardSpinnerContainer ->` +export const ProjectCardSpinnerContainer = styled.div` position: absolute; height: 100%; width: 100%; @@ -137,9 +106,7 @@ interface IProjectCardContentContainer { duration: number; } -export const ProjectCardContentContainer = styled.div< - IProjectCardContentContainer ->` +export const ProjectCardContentContainer = styled.div` position: absolute; height: 100%; width: 100%; diff --git a/src/components/home/home.tsx b/src/components/home/home.tsx index 32f4b6c6..a9086ed8 100644 --- a/src/components/home/home.tsx +++ b/src/components/home/home.tsx @@ -1,7 +1,6 @@ -import React, { useCallback, useEffect, useState } from "react"; -import { useDispatch, useSelector } from "react-redux"; -import ReactTooltip from "react-tooltip"; -import Header from "@comp/header/header"; +import { useCallback, useEffect, useState } from "react"; +import { useDispatch, useSelector } from "@root/store"; +import { Header } from "@comp/header/header"; import Search from "./search"; import PopularProjects from "./popular-projects"; import RandomProjects from "./random-projects"; @@ -12,12 +11,12 @@ import { selectPopularProjectsSlice } from "./selectors"; -const Home = (): React.ReactElement => { - ReactTooltip.rebuild(); +const Home = () => { const dispatch = useDispatch(); - const [popularProjectsFetchOffset, popularProjectsTotalRecords] = - useSelector(selectPopularProjectsFetchOffset); + const popularProjectsFetchOffset = useSelector( + selectPopularProjectsFetchOffset + ); const [currentPopularProjectsOffset, setCurrentPopularProjectsOffset] = useState(0); @@ -32,36 +31,23 @@ const Home = (): React.ReactElement => { ); const handlePopularProjectsNextPage = useCallback(() => { - if ( - popularProjectsTotalRecords > 0 && - currentPopularProjectsOffset < popularProjectsTotalRecords - ) { - let popularProjects; - try { - popularProjects = fetchPopularProjects( - currentPopularProjectsOffset - ); - } catch (error) { - console.error(error); - } - if (popularProjects) { - dispatch(popularProjects); - setCurrentPopularProjectsOffset( - currentPopularProjectsOffset + 8 - ); - } + try { + dispatch(fetchPopularProjects(currentPopularProjectsOffset)); + } catch (error) { + console.error(error); } - }, [dispatch, popularProjectsTotalRecords, currentPopularProjectsOffset]); + // if (popularProjects) { + // dispatch(popularProjects); + // setCurrentPopularProjectsOffset( + // currentPopularProjectsOffset + 8 + // ); + // } + }, [dispatch, currentPopularProjectsOffset]); const handlePopularProjectsPreviousPage = useCallback(() => { - if ( - popularProjectsTotalRecords > 0 && - currentPopularProjectsOffset > 0 - ) { - dispatch(fetchPopularProjects(currentPopularProjectsOffset)); - setCurrentPopularProjectsOffset(currentPopularProjectsOffset - 8); - } - }, [dispatch, popularProjectsTotalRecords, currentPopularProjectsOffset]); + dispatch(fetchPopularProjects(currentPopularProjectsOffset)); + setCurrentPopularProjectsOffset(currentPopularProjectsOffset - 8); + }, [dispatch, currentPopularProjectsOffset]); useEffect(() => { if (popularProjectsFetchOffset < 0) { @@ -74,6 +60,7 @@ const Home = (): React.ReactElement => { window.scrollTo(0, 0); const rootElement = document.querySelector("#root"); rootElement && rootElement.scrollTo(0, 0); + document.title = "Csound Web-IDE"; }, []); return ( @@ -81,7 +68,8 @@ const Home = (): React.ReactElement => {
- Search is being fixed...

*/} + {/* { handlePopularProjectsPreviousPage={ handlePopularProjectsPreviousPage } - hasNext={ - popularProjectsTotalRecords > 0 && - currentPopularProjectsOffset < - popularProjectsTotalRecords - } hasPrevious={currentPopularProjectsOffset > 0} - /> + /> */}
diff --git a/src/components/home/popular-projects.tsx b/src/components/home/popular-projects.tsx index a173b604..bc04268d 100644 --- a/src/components/home/popular-projects.tsx +++ b/src/components/home/popular-projects.tsx @@ -1,41 +1,33 @@ import React from "react"; +import { RootState, useSelector } from "@root/store"; import { isEmpty, path, range } from "ramda"; -import { useSelector } from "react-redux"; -import { IStore } from "@store/types"; import { IProject } from "@comp/projects/types"; -import LeftIcon from "@material-ui/icons/ArrowBack"; -import RightIcon from "@material-ui/icons/ArrowForward"; -import IconButton from "@material-ui/core/IconButton"; +import LeftIcon from "@mui/icons-material/ArrowBack"; +import RightIcon from "@mui/icons-material/ArrowForward"; +import IconButton from "@mui/material/IconButton"; import { Theme, useTheme } from "@emotion/react"; -import ProjectCard, { ProjectCardSkeleton } from "./project-card"; +import { ProjectCard, ProjectCardSkeleton } from "./project-card"; +import { PopularProjectResponse } from "./types"; import * as SS from "./styles"; const PopularProjects = ({ projects, handlePopularProjectsNextPage, handlePopularProjectsPreviousPage, - hasNext, hasPrevious }: { - projects: IProject[]; + projects: PopularProjectResponse[]; handlePopularProjectsNextPage: () => void; handlePopularProjectsPreviousPage: () => void; - hasNext: boolean; hasPrevious: boolean; }): React.ReactElement => { const theme: Theme = useTheme(); - const profiles = useSelector((store: IStore) => { + const profiles = useSelector((store: RootState) => { return path(["HomeReducer", "profiles"], store); }); - const isLoading = useSelector((store: IStore) => { - const totalRecords = path( - ["HomeReducer", "popularProjectsTotalRecords"], - store - ); - return totalRecords < 0 || (isEmpty(projects) && totalRecords > 0); - }); + const isLoading = false; return ( <> @@ -53,9 +45,9 @@ const PopularProjects = ({ diff --git a/src/components/home/project-card.tsx b/src/components/home/project-card.tsx index 5f52594c..7dba2a5f 100644 --- a/src/components/home/project-card.tsx +++ b/src/components/home/project-card.tsx @@ -2,7 +2,7 @@ import React from "react"; import { Bars as BarsSpinner } from "react-loader-spinner"; import { Theme } from "@emotion/react"; import ProjectAvatar from "@elem/project-avatar"; -import ListPlayButton from "@comp/profile/list-play-button"; +import { ListPlayButton } from "@comp/profile/list-play-button"; import { IProject } from "@comp/projects/types"; import { IProfile } from "@comp/profile/types"; import { @@ -19,13 +19,10 @@ import { Photo, ProjectCardContentBottomID } from "./home-ui"; +import { RandomProjectResponse, PopularProjectResponse } from "./types"; import * as SS from "./styles"; -export const ProjectCardSkeleton = ({ - theme -}: { - theme: Theme; -}): React.ReactElement => ( +export const ProjectCardSkeleton = ({ theme }: { theme: Theme }) => (
@@ -34,19 +31,23 @@ export const ProjectCardSkeleton = ({
); -const ProjectCard = ({ +export const ProjectCard = ({ projectIndex, profile, project }: { projectIndex: number; profile: IProfile; - project: IProject; -}): React.ReactElement => { + project: IProject | RandomProjectResponse | PopularProjectResponse; +}) => { return (
- +
@@ -58,7 +59,12 @@ const ProjectCard = ({ - + @@ -77,5 +83,3 @@ const ProjectCard = ({
); }; - -export default ProjectCard; diff --git a/src/components/home/random-projects.tsx b/src/components/home/random-projects.tsx index c5198c61..0029a0d9 100644 --- a/src/components/home/random-projects.tsx +++ b/src/components/home/random-projects.tsx @@ -1,42 +1,38 @@ -import React from "react"; -import IconButton from "@material-ui/core/IconButton"; -import ShuffleIcon from "@material-ui/icons/Shuffle"; -import { path, range } from "ramda"; -import { useDispatch, useSelector } from "react-redux"; +import { useEffect, useState } from "react"; +import { RootState, useDispatch, useSelector } from "@root/store"; +import IconButton from "@mui/material/IconButton"; +import ShuffleIcon from "@mui/icons-material/Shuffle"; +import { range } from "ramda"; import { fetchRandomProjects } from "./actions"; -import { IStore } from "@store/types"; import { useTheme } from "@emotion/react"; -import ProjectCard, { ProjectCardSkeleton } from "./project-card"; +import { RandomProjectResponse } from "./types"; +import { ProjectCard, ProjectCardSkeleton } from "./project-card"; import * as SS from "./styles"; -const RandomProjects = (): React.ReactElement => { +const RandomProjects = () => { const dispatch = useDispatch(); - const [isMounted, setIsMounted] = React.useState(false); + const [isMounted, setIsMounted] = useState(false); - React.useEffect(() => { + useEffect(() => { if (!isMounted) { setIsMounted(true); - let randomProjects; - try { - randomProjects = fetchRandomProjects(); - } catch (error) { - console.error(error); - } - randomProjects && dispatch(randomProjects); + dispatch(fetchRandomProjects()); } }, [dispatch, isMounted, setIsMounted]); - const theme = useTheme(); - const profiles = useSelector((store: IStore) => { - return path(["HomeReducer", "profiles"], store); + const theme = useTheme(); + const profiles = useSelector((store: RootState) => { + return store.HomeReducer.profiles; }); - const randomProjects = useSelector((store: IStore) => { - return path(["HomeReducer", "randomProjects"], store); - }); + const randomProjects: RandomProjectResponse[] = useSelector( + (store: RootState) => { + return store.HomeReducer.randomProjects; + } + ); - const randomProjectsLoading = useSelector((store: IStore) => { - return path(["HomeReducer", "randomProjectsLoading"], store); + const randomProjectsLoading: boolean = useSelector((store: RootState) => { + return store.HomeReducer.randomProjectsLoading; }); return ( @@ -61,12 +57,13 @@ const RandomProjects = (): React.ReactElement => { )) : randomProjects.map((project, index) => { - return profiles[project.userUid] ? ( + const profile = profiles[project.userUid]; + return profile ? ( ) : ( diff --git a/src/components/home/reducer.ts b/src/components/home/reducer.ts index 7ad7f18a..9a62422c 100644 --- a/src/components/home/reducer.ts +++ b/src/components/home/reducer.ts @@ -1,4 +1,3 @@ -import { assoc, concat, isEmpty, mergeAll, pipe, when } from "ramda"; import { HomeActionTypes, ADD_USER_PROFILES, @@ -7,29 +6,35 @@ import { ADD_POPULAR_PROJECTS, ADD_RANDOM_PROJECTS, SET_POPULAR_PROJECTS_OFFSET, - SET_RANDOM_PROJECTS_LOADING + SET_RANDOM_PROJECTS_LOADING, + AddPopularProjectsAction, + AddUserProfiles, + AddRandomProjectsAction, + SetPopularProjectsOffsetAction, + SetRandomProjectsLoading, + SearchProjectsRequest, + SearchProjectsSuccess } from "./types"; import { IProject } from "@comp/projects/types"; import { IProfile } from "@comp/profile/types"; +import { RandomProjectResponse, PopularProjectResponse } from "./types"; export interface IHomeReducer { - popularProjects: IProject[]; + popularProjects: PopularProjectResponse[]; popularProjectsOffset: number; - popularProjectsTotalRecords: number; profiles: { [uid: string]: IProfile }; searchProjectsRequest: boolean; searchResult: IProject[]; searchResultTotalRecords: number; searchPaginationOffset: number; searchQuery: string; - randomProjects: IProject[]; + randomProjects: RandomProjectResponse[]; randomProjectsLoading: boolean; } const INITIAL_STATE: IHomeReducer = { popularProjects: [], popularProjectsOffset: -1, - popularProjectsTotalRecords: -1, profiles: {}, searchProjectsRequest: false, searchResult: [], @@ -41,59 +46,73 @@ const INITIAL_STATE: IHomeReducer = { }; const HomeReducer = ( - state: IHomeReducer = INITIAL_STATE, - action: HomeActionTypes + state: IHomeReducer | undefined, + unknownAction: HomeActionTypes ): IHomeReducer => { - switch (action.type) { + if (!state) { + return INITIAL_STATE; + } + + switch (unknownAction.type) { case SEARCH_PROJECTS_REQUEST: { - return pipe( - assoc("searchProjectsRequest", !isEmpty(action.query)), - assoc("searchQuery", action.query), - assoc( - "searchPaginationOffset", - isEmpty(action.query) ? -1 : action.offset - ), - when( - () => isEmpty(action.query), - assoc("searchResultTotalRecords", -1) - ) - )(state); + const newState: IHomeReducer = { ...state }; + const action = unknownAction as SearchProjectsRequest; + newState.searchProjectsRequest = action.query.length > 0; + newState.searchQuery = action.query; + newState.searchPaginationOffset = + action.query.length === 0 ? -1 : action.offset; + if (action.query.length === 0) { + newState.searchResultTotalRecords = -1; + } + return newState; } case SEARCH_PROJECTS_SUCCESS: { - return pipe( - assoc("searchResult", action.result || []), - assoc("searchProjectsRequest", false), - assoc("searchResultTotalRecords", action.totalRecords) - )(state); + const action = unknownAction as SearchProjectsSuccess; + return { + ...state, + searchResult: action.result || [], + searchProjectsRequest: false, + searchResultTotalRecords: action.totalRecords + }; } case ADD_USER_PROFILES: { - return assoc( - "profiles", - mergeAll([action.payload, state.profiles]), - state - ); + const action = unknownAction as AddUserProfiles; + return { + ...state, + profiles: { + ...state.profiles, + ...action.payload + } + }; } case ADD_POPULAR_PROJECTS: { - return pipe( - assoc( - "popularProjects", - concat(state.popularProjects, action.payload) - ), - assoc("popularProjectsTotalRecords", action.totalRecords) - )(state); + const action = unknownAction as AddPopularProjectsAction; + return { + ...state, + popularProjects: [...state.popularProjects, ...action.payload] + }; } case SET_POPULAR_PROJECTS_OFFSET: { - return assoc("popularProjectsOffset", action.newOffset, state); + const action = unknownAction as SetPopularProjectsOffsetAction; + return { + ...state, + popularProjectsOffset: action.newOffset + }; } - case ADD_RANDOM_PROJECTS: { - return assoc("randomProjects", action.payload || [])(state); + const action = unknownAction as AddRandomProjectsAction; + return { + ...state, + randomProjects: action.payload || [] + }; } - case SET_RANDOM_PROJECTS_LOADING: { - return assoc("randomProjectsLoading", action.isLoading)(state); + const action = unknownAction as SetRandomProjectsLoading; + return { + ...state, + randomProjectsLoading: action.isLoading + }; } - default: { return state; } diff --git a/src/components/home/search.tsx b/src/components/home/search.tsx index a17c1f3c..52953971 100644 --- a/src/components/home/search.tsx +++ b/src/components/home/search.tsx @@ -1,51 +1,50 @@ import React from "react"; +import { RootState, useDispatch, useSelector } from "@root/store"; import { ThreeDots } from "react-loader-spinner"; import { useTheme } from "@emotion/react"; import { isEmpty, path } from "ramda"; import { searchProjects } from "./actions"; import { selectSearchResult } from "./selectors"; -import { IStore } from "@store/types"; import { IProject } from "@comp/projects/types"; -import TextField from "@material-ui/core/TextField"; -import { useDispatch, useSelector } from "react-redux"; +import TextField from "@mui/material/TextField"; import { debounce } from "throttle-debounce"; -import LeftIcon from "@material-ui/icons/ArrowBack"; -import RightIcon from "@material-ui/icons/ArrowForward"; -import IconButton from "@material-ui/core/IconButton"; -import ProjectCard from "./project-card"; +import LeftIcon from "@mui/icons-material/ArrowBack"; +import RightIcon from "@mui/icons-material/ArrowForward"; +import IconButton from "@mui/material/IconButton"; +import { ProjectCard } from "./project-card"; import * as SS from "./styles"; const doSearch = debounce(100, (query, offset, dispatch) => { dispatch(searchProjects(query, offset)); }); -const Search = (): React.ReactElement => { +const Search = () => { const dispatch = useDispatch(); const theme = useTheme(); const searchResult: IProject[] = useSelector(selectSearchResult); - const profiles = useSelector((store: IStore) => { + const profiles = useSelector((store: RootState) => { return path(["HomeReducer", "profiles"], store); }); - const searchQuery = useSelector((store: IStore) => { + const searchQuery = useSelector((store: RootState) => { return path(["HomeReducer", "searchQuery"], store); }); - const searchProjectsRequest = useSelector((store: IStore) => { + const searchProjectsRequest = useSelector((store: RootState) => { return path(["HomeReducer", "searchProjectsRequest"], store); }); - const searchPaginationOffset = useSelector((store: IStore) => { + const searchPaginationOffset = useSelector((store: RootState) => { return path(["HomeReducer", "searchPaginationOffset"], store); }); - const searchResultTotalRecords = useSelector((store: IStore) => { + const searchResultTotalRecords = useSelector((store: RootState) => { return path(["HomeReducer", "searchResultTotalRecords"], store); }); const onChange = React.useCallback( - (event) => { + (event: React.ChangeEvent) => { doSearch(event.target.value, 0, dispatch); }, [dispatch] @@ -73,7 +72,8 @@ const Search = (): React.ReactElement => { disabled={ searchPaginationOffset < 1 || searchResultTotalRecords < 1 || - isEmpty(searchQuery) + isEmpty(searchQuery) || + searchResultTotalRecords <= 8 } > @@ -93,6 +93,7 @@ const Search = (): React.ReactElement => { searchPaginationOffset < 0 || searchResultTotalRecords < 0 || isEmpty(searchQuery) || + searchResultTotalRecords <= 8 || !( searchPaginationOffset < searchResultTotalRecords @@ -114,6 +115,34 @@ const Search = (): React.ReactElement => { label="Search field" type="search" variant="outlined" + sx={{ + "& .MuiInputBase-root": { + '& input[type="search"]::-webkit-search-cancel-button': + { + WebkitAppearance: "none", + appearance: "none", + height: "20px", + width: "20px", + borderRadius: "50%", + background: `url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23999'%3E%3Cpath d='M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z'/%3E%3C/svg%3E") no-repeat center`, + backgroundSize: "16px 16px", + cursor: "pointer", + opacity: 0.6, + transition: "opacity 0.2s ease", + "&:hover": { + opacity: 1, + background: `url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23fff'%3E%3Cpath d='M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z'/%3E%3C/svg%3E") no-repeat center`, + backgroundSize: "16px 16px" + } + }, + '& input[type="search"]:not(:placeholder-shown)::-webkit-search-cancel-button': + { + background: `url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23fff'%3E%3Cpath d='M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z'/%3E%3C/svg%3E") no-repeat center`, + backgroundSize: "16px 16px", + opacity: 0.8 + } + } + }} />
{ +export const selectPopularProjectsFetchOffset = (store: RootState): number => { const state: IHomeReducer = store.HomeReducer; - return [state.popularProjectsOffset, state.popularProjectsTotalRecords]; + return state.popularProjectsOffset; }; -export const selectPopularProjectsSlice = (from: number, to: number) => ( - store: IStore -): IProject[] => { - const state: IHomeReducer = store.HomeReducer; - const popularProjects = state.popularProjects; - return slice(from, to, popularProjects); -}; +export const selectPopularProjects = ( + state: RootState +): PopularProjectResponse[] => state.HomeReducer.popularProjects; + +export const selectPopularProjectsSlice = (from: number, to: number) => + createSelector( + [selectPopularProjects], + (popularProjects: PopularProjectResponse[]) => + popularProjects.slice(from, to) + ); -export const selectSearchResult = (store: IStore): IProject[] => { +export const selectSearchResult = (store: RootState): IProject[] => { const state: IHomeReducer = store.HomeReducer; return state.searchResult; }; diff --git a/src/components/home/types.ts b/src/components/home/types.ts index 7255b906..3340f319 100644 --- a/src/components/home/types.ts +++ b/src/components/home/types.ts @@ -1,4 +1,6 @@ import { IProject } from "@comp/projects/types"; +import { Timestamp } from "firebase/firestore"; +import { UnknownAction } from "redux"; export const SEARCH_PROJECTS_REQUEST = "HOME.SEARCH_PROJECTS_REQUEST"; export const SEARCH_PROJECTS_SUCCESS = "HOME.SEARCH_PROJECTS_SUCCESS"; @@ -9,45 +11,69 @@ export const ADD_POPULAR_PROJECTS = "HOME.ADD_POPULAR_PROJECTS"; export const SET_POPULAR_PROJECTS_OFFSET = "HOME.SET_POPULAR_PROJECTS_OFFSET"; export const SET_RANDOM_PROJECTS_LOADING = "HOME.SET_RANDOM_PROJECTS_LOADING"; -interface SearchProjectsRequest { +export interface RandomProjectResponse { + created: Timestamp; + description: string; + iconBackgroundColor: string | undefined; + iconForegroundColor: string | undefined; + iconName: string | undefined; + name: string; + projectUid: string; + public: boolean; + userUid: string; +} + +export interface PopularProjectResponse { + created: Timestamp; + description: string; + iconBackgroundColor: string | undefined; + iconForegroundColor: string | undefined; + iconName: string | undefined; + name: string; + projectUid: string; + public: boolean; + userUid: string; +} + +export interface SearchProjectsRequest { type: typeof SEARCH_PROJECTS_REQUEST; query: string; offset: number; } -interface SearchProjectsSuccess { +export interface SearchProjectsSuccess { type: typeof SEARCH_PROJECTS_SUCCESS; result: IProject[]; totalRecords: number; } -interface AddUserProfiles { +export interface AddUserProfiles { type: typeof ADD_USER_PROFILES; payload: any; } -interface AddPopularProjectsAction { +export interface AddPopularProjectsAction { type: typeof ADD_POPULAR_PROJECTS; - payload: IProject[]; - totalRecords: number; + payload: PopularProjectResponse[]; } -interface SetPopularProjectsOffsetAction { +export interface SetPopularProjectsOffsetAction { type: typeof SET_POPULAR_PROJECTS_OFFSET; newOffset: number; } -interface AddRandomProjectsAction { +export interface AddRandomProjectsAction { type: typeof ADD_RANDOM_PROJECTS; - payload: IProject[]; + payload: RandomProjectResponse[]; } -interface SetRandomProjectsLoading { +export interface SetRandomProjectsLoading { type: typeof SET_RANDOM_PROJECTS_LOADING; isLoading: boolean; } export type HomeActionTypes = + | UnknownAction | SearchProjectsRequest | SearchProjectsSuccess | AddUserProfiles diff --git a/src/components/hot-keys/actions.ts b/src/components/hot-keys/actions.ts index a5310c1b..2f985f6a 100644 --- a/src/components/hot-keys/actions.ts +++ b/src/components/hot-keys/actions.ts @@ -1,13 +1,8 @@ import "firebase/auth"; -import { - IEditorCallbacks, - IProjectEditorCallbacks, - STORE_EDITOR_KEYBOARD_CALLBACKS, - STORE_PROJECT_EDITOR_KEYBOARD_CALLBACKS -} from "./types"; -import { path, pathOr } from "ramda"; +import { RootState, store } from "@root/store"; +import { EditorView } from "@codemirror/view"; import { Transaction } from "@codemirror/state"; -import { IStore } from "@store/types"; +import { pathOr } from "ramda"; import { newDocument, saveAllAndClose, @@ -24,10 +19,13 @@ import { getPlayActionFromProject, getPlayActionFromTarget } from "@comp/target-controls/utils"; -import { pauseCsound, stopCsound } from "@comp/csound/actions"; +import { csoundInstance, pauseCsound, stopCsound } from "@comp/csound/actions"; import { filenameToCsoundType } from "@comp/csound/utils"; +import { openEditors } from "@comp/editor"; import { editorEvalCode } from "@comp/editor/utils"; import * as EditorActions from "@comp/editor/actions"; +import { keyboardCallbacks } from "./index"; +import { UPDATE_COUNTER } from "./types"; const withPreventDefault = (callback: any) => @@ -36,65 +34,80 @@ const withPreventDefault = callback(); }; -export const storeProjectEditorKeyboardCallbacks = ( - projectUid: string -): ((dispatch: (any) => void, getStore: () => IStore) => Promise) => { - return async (dispatch, getStore) => { - const callbacks: IProjectEditorCallbacks = { - add_file: withPreventDefault(() => - dispatch(addDocument(projectUid)) - ), - new_document: withPreventDefault(() => - dispatch(newDocument(projectUid, "")) - ), - open_target_config_dialog: withPreventDefault(() => - dispatch(showTargetsConfigDialog()) - ), - pause_playback: withPreventDefault(() => dispatch(pauseCsound())), - run_project: withPreventDefault(() => { - const playActionDefault = (getPlayActionFromTarget as any)( - getStore() - ); - const playActionFallback = getPlayActionFromProject( - projectUid, - getStore() - ); - const playAction = playActionDefault || playActionFallback; - - if (playAction) { - const isOwner = projectUid - ? selectIsOwner(projectUid)(getStore()) - : false; - if (isOwner) { - dispatch(saveAllFiles()); - } - dispatch(playAction); +export const storeProjectEditorKeyboardCallbacks = (projectUid: string) => { + keyboardCallbacks.set( + "add_file", + withPreventDefault(() => store.dispatch(addDocument(projectUid))) + ); + keyboardCallbacks.set( + "new_document", + withPreventDefault(() => store.dispatch(newDocument(projectUid, ""))) + ); + keyboardCallbacks.set( + "open_target_config_dialog", + withPreventDefault(() => store.dispatch(showTargetsConfigDialog())) + ); + keyboardCallbacks.set( + "pause_playback", + withPreventDefault(() => store.dispatch(pauseCsound())) + ); + keyboardCallbacks.set( + "run_project", + withPreventDefault(() => { + const playActionDefault = getPlayActionFromTarget(projectUid)( + store.getState() + ); + const playActionFallback = getPlayActionFromProject( + projectUid, + store.getState() + ); + const playAction = playActionDefault || playActionFallback; + + if (playAction) { + const isOwner = projectUid + ? selectIsOwner(store.getState()) + : false; + if (isOwner) { + store.dispatch(saveAllFiles()); } - }), - save_all_documents: withPreventDefault(() => - dispatch(saveAllFiles()) - ), - save_and_close: withPreventDefault(() => - dispatch(saveAllAndClose("/")) - ), - save_document: withPreventDefault(() => dispatch(saveFile())), - stop_playback: withPreventDefault(() => dispatch(stopCsound())) - }; - dispatch({ - type: STORE_PROJECT_EDITOR_KEYBOARD_CALLBACKS, - callbacks - }); - }; + store.dispatch(playAction as any); + } + }) + ); + keyboardCallbacks.set( + "save_all_documents", + withPreventDefault(() => store.dispatch(saveAllFiles())) + ); + keyboardCallbacks.set( + "save_and_close", + withPreventDefault(() => saveAllAndClose(store.dispatch, "/")) + ); + keyboardCallbacks.set( + "save_document", + withPreventDefault(() => store.dispatch(saveFile())) + ); + keyboardCallbacks.set( + "stop_playback", + withPreventDefault(() => store.dispatch(stopCsound())) + ); + + store.dispatch({ type: UPDATE_COUNTER }); }; -const selectCurrentEditor = (store): any | undefined => { +const selectCurrentEditor = (store: RootState): EditorView | undefined => { const currentTab = selectCurrentTab(store); - return currentTab && currentTab.editorInstance; + const currentDocId = currentTab && currentTab.uid; + if (currentDocId && openEditors.has(currentDocId)) { + return openEditors.get(currentDocId); + } }; -const selectDocumentName = (store, projectUid): any | undefined => { +const selectDocumentName = ( + store: RootState, + projectUid: string +): string | undefined => { const currentTab = selectCurrentTab(store); - if (currentTab && currentTab.editorInstance) { + if (currentTab) { const documentUid = currentTab.uid; if (documentUid) { const document = @@ -106,120 +119,107 @@ const selectDocumentName = (store, projectUid): any | undefined => { } }; -export const storeEditorKeyboardCallbacks = ( - projectUid: string -): ((dispatch: (any) => void, getStore: () => IStore) => Promise) => { - return async (dispatch, getStore) => { - const callbacks: IEditorCallbacks = { - doc_at_point: withPreventDefault(() => { - const editor = selectCurrentEditor(getStore()); - editor && dispatch(EditorActions.manualEntryAtPoint(editor)); - }), - // find_simple: withPreventDefault(() => { - // const editor = selectCurrentEditor(getStore()); - - // const searchField = document.querySelector( - // ".CodeMirror-dialog.CodeMirror-dialog-top" - // ); - // if (!searchField) { - // editor && editor.execCommand("findPersistent"); - // const maybeDialog = document.querySelector( - // ".CodeMirror-search-field" - // ); - // setTimeout(() => { - // maybeDialog && maybeDialog[0] && maybeDialog[0].focus(); - // }, 100); - // } else { - // const dialogTop = document.querySelector( - // ".CodeMirror-dialog-top" - // ); - // dialogTop && dialogTop.remove(); - // editor && editor.focus(); - // } - // }), - undo: withPreventDefault(() => { - const editor = selectCurrentEditor(getStore()); - editor && editor.dispatch(Transaction.userEvent.of("undo")); - }), - redo: withPreventDefault(() => { - const editor = selectCurrentEditor(getStore()); - editor && editor.dispatch(Transaction.userEvent.of("redo")); - }), - eval_block: withPreventDefault(() => { - const storeState = getStore(); - const editor = selectCurrentEditor(storeState); - const csound = path(["csound", "csound"], storeState); - - const csoundStatus = pathOr( - "stopped", - ["csound", "status"], - storeState - ); - const documentName = selectDocumentName(storeState, projectUid); - const documentType = - documentName && filenameToCsoundType(documentName); - - if (editor && csound && documentName && documentType) { - editorEvalCode( - csound, - csoundStatus, - documentType, - editor, - true - ); - } - }), - eval: withPreventDefault(() => { - const storeState = getStore(); - const editor = selectCurrentEditor(storeState); - const csound = path(["csound", "csound"], storeState); - - const csoundStatus = pathOr( - "stopped", - ["csound", "status"], - storeState +export const storeEditorKeyboardCallbacks = (projectUid: string) => { + keyboardCallbacks.set( + "doc_at_point", + withPreventDefault(() => { + const editor = selectCurrentEditor(store.getState()); + editor && store.dispatch(EditorActions.manualEntryAtPoint(editor)); + }) + ); + + keyboardCallbacks.set( + "undo", + withPreventDefault(() => { + const editor = selectCurrentEditor(store.getState()); + editor && + editor.dispatch({ + annotations: Transaction.userEvent.of("undo") + }); + }) + ); + + keyboardCallbacks.set( + "redo", + withPreventDefault(() => { + const editor = selectCurrentEditor(store.getState()); + editor && + editor.dispatch({ + annotations: Transaction.userEvent.of("redo") + }); + }) + ); + + // keyboardCallbacks.set( + // "toggle_comment", + // withPreventDefault(() => { + // const editor = selectCurrentEditor(store.getState()); + // if (editor && typeof editor.toggleComment === "function") { + // editor.toggleComment(); + // } + // }) + // ); + + keyboardCallbacks.set( + "eval_block", + withPreventDefault(() => { + const storeState = store.getState(); + const editor = selectCurrentEditor(storeState); + const csound = csoundInstance; + + const csoundStatus = pathOr( + "stopped", + ["csound", "status"], + storeState + ); + const documentName = selectDocumentName(storeState, projectUid); + const documentType = + documentName && filenameToCsoundType(documentName); + + if (editor && csound && documentName && documentType) { + editorEvalCode( + csound, + csoundStatus, + documentType, + editor, + true ); - const documentName = selectDocumentName(storeState, projectUid); - const documentType = - documentName && filenameToCsoundType(documentName); - - if (editor && csound && documentName && documentType) { - editorEvalCode( - csound, - csoundStatus, - documentType, - editor, - false - ); - } - }), - toggle_comment: withPreventDefault(() => { - const storeState = getStore(); - const editor = selectCurrentEditor(storeState); - if (editor && typeof editor.toggleComment === "function") { - editor.toggleComment(); - } - }) - }; + } + }) + ); - dispatch({ - type: STORE_EDITOR_KEYBOARD_CALLBACKS, - callbacks - }); - }; + keyboardCallbacks.set( + "eval", + withPreventDefault(() => { + const storeState = store.getState(); + const editor = selectCurrentEditor(storeState); + const csound = csoundInstance; + + const csoundStatus = pathOr( + "stopped", + ["csound", "status"], + storeState + ); + const documentName = selectDocumentName(storeState, projectUid); + const documentType = + documentName && filenameToCsoundType(documentName); + + if (editor && csound && documentName && documentType) { + editorEvalCode( + csound, + csoundStatus, + documentType, + editor, + false + ); + } + }) + ); }; -export const invokeHotKeyCallback = ( - hotKey: string -): ((dispatch: (any) => void, getStore: () => IStore) => Promise) => { - return async (dispatch, getState) => { - const state = getState(); - const maybeCallback = path( - ["HotKeysReducer", "callbacks", hotKey], - state - ); - if (typeof maybeCallback === "function") { - maybeCallback(); - } - }; +export const invokeHotKeyCallback = (hotKey: string) => { + if (keyboardCallbacks.has(hotKey)) { + const callback = keyboardCallbacks.get(hotKey) as any; + callback(); + } }; diff --git a/src/components/hot-keys/hot-keys.tsx b/src/components/hot-keys/hot-keys.tsx index 1e77f201..db4712d5 100644 --- a/src/components/hot-keys/hot-keys.tsx +++ b/src/components/hot-keys/hot-keys.tsx @@ -1,9 +1,10 @@ -import React from "react"; +import React, { useMemo } from "react"; import { configure, GlobalHotKeys, KeyMap } from "react-hotkeys"; -import { selectKeyCallbacks, selectKeyBindings } from "./selectors"; import { useSelector } from "react-redux"; -import { assoc, isNil, keys, prop, reduce } from "ramda"; +import { assoc, reduce } from "ramda"; import { IHotKeysCallbacks } from "./types"; +import { keyboardCallbacks } from "./index"; +import { RootState } from "@root/store"; configure({ // logLevel: "verbose", @@ -11,39 +12,40 @@ configure({ stopEventPropagationAfterHandling: true, stopEventPropagationAfterIgnoring: true, allowCombinationSubmatches: false, - ignoreEventsCondition: (event) => { + ignoreEventsCondition: () => { return false; }, ignoreTags: [] }); -type HotKeyHandler = (keyEvent?: KeyboardEvent) => void; +// type HotKeyHandler = (keyEvent?: KeyboardEvent) => void; type CommandKey = keyof IHotKeysCallbacks; const HotKeys = ({ children }: { - children: React.ReactElement; + children: React.ReactElement | React.ReactElement[]; }): React.ReactElement => { // prevent leak into the manual iframe const insideIframe = !!window.frameElement; - const callbacks = useSelector(selectKeyCallbacks); - const bindings: KeyMap = useSelector(selectKeyBindings) as KeyMap; + const bindings: KeyMap = useSelector( + (store: RootState) => store.HotKeysReducer.bindings + ) as KeyMap; + const updateCounter: number = useSelector( + (store: RootState) => store.HotKeysReducer.updateCounter + ); + // all callbacks that aren't bound must be noop callbacks - const safeCallbacks = reduce( - (accumulator, k: CommandKey) => - assoc( - k, - isNil(callbacks && prop(k, callbacks)) - ? (((event: any) => { - event && event.preventDefault(); - }) as HotKeyHandler) - : callbacks && prop(k, callbacks), - accumulator + const safeCallbacks = useMemo( + () => + reduce( + (accumulator, k: CommandKey) => + assoc(k, keyboardCallbacks.get(k), accumulator), + {}, + [...keyboardCallbacks.keys()] ), - {}, - keys(callbacks || {}) + [updateCounter] ); return ( <> diff --git a/src/components/hot-keys/index.ts b/src/components/hot-keys/index.ts new file mode 100644 index 00000000..afa71413 --- /dev/null +++ b/src/components/hot-keys/index.ts @@ -0,0 +1 @@ +export const keyboardCallbacks = new Map(); diff --git a/src/components/hot-keys/reducer.ts b/src/components/hot-keys/reducer.ts index 12e25034..ecff64c4 100644 --- a/src/components/hot-keys/reducer.ts +++ b/src/components/hot-keys/reducer.ts @@ -1,59 +1,31 @@ import defaultBindings from "./default-bindings"; -import { assoc, mergeAll } from "ramda"; -import { - HotKeysActionTypes, - IHotKeysCallbacks, - BindingsMap, - STORE_EDITOR_KEYBOARD_CALLBACKS, - STORE_PROJECT_EDITOR_KEYBOARD_CALLBACKS -} from "./types"; +import { assoc } from "ramda"; +import { HotKeysActionTypes, BindingsMap, UPDATE_COUNTER } from "./types"; export interface IHotKeys { bindings: BindingsMap; - callbacks: IHotKeysCallbacks; + updateCounter: number; } const INITIAL_STATE: IHotKeys = { - bindings: defaultBindings, - callbacks: { - // IProfileCommands - new_project: undefined, - // IProjectEditorCommands - add_file: undefined, - new_document: undefined, - open_target_config_dialog: undefined, - pause_playback: undefined, - run_project: undefined, - save_all_documents: undefined, - save_and_close: undefined, - save_document: undefined, - stop_playback: undefined, - // IEditorCommands - doc_at_point: undefined - } + updateCounter: 0, + bindings: defaultBindings }; const HotKeysReducer = ( - state: IHotKeys, + state: IHotKeys | undefined, action: HotKeysActionTypes ): IHotKeys => { + if (!state) { + return INITIAL_STATE; + } + switch (action.type) { - case STORE_PROJECT_EDITOR_KEYBOARD_CALLBACKS: { - return assoc( - "callbacks", - mergeAll([state.callbacks, action.callbacks]), - state - ); - } - case STORE_EDITOR_KEYBOARD_CALLBACKS: { - return assoc( - "callbacks", - mergeAll([state.callbacks, action.callbacks]), - state - ); + case UPDATE_COUNTER: { + return assoc("updateCounter", state.updateCounter + 1, state); } default: { - return state || INITIAL_STATE; + return state; } } }; diff --git a/src/components/hot-keys/selectors.ts b/src/components/hot-keys/selectors.ts deleted file mode 100644 index e8ad4462..00000000 --- a/src/components/hot-keys/selectors.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { BindingsMap, IHotKeysCallbacks } from "./types"; -import { path } from "ramda"; - -export const selectKeyCallbacks: ( - Selector -) => IHotKeysCallbacks | undefined = path(["HotKeysReducer", "callbacks"]); - -export const selectKeyBindings: (Selector) => BindingsMap | undefined = path([ - "HotKeysReducer", - "bindings" -]); - -// export const selectKeyMaps: Selector = path(["HotKeysReducer", "keyMap"]); diff --git a/src/components/hot-keys/types.ts b/src/components/hot-keys/types.ts index 6142117c..807636d0 100644 --- a/src/components/hot-keys/types.ts +++ b/src/components/hot-keys/types.ts @@ -7,15 +7,16 @@ import { const PREFIX = "HOTKEYS."; // export const SET_MENU_BAR_HOTKEYS = PREFIX + "SET_MENU_BAR_HOTKEYS"; -export const STORE_PROJECT_EDITOR_KEYBOARD_CALLBACKS = - PREFIX + "STORE_PROJECT_EDITOR_KEYBOARD_CALLBACKS"; +// export const STORE_PROJECT_EDITOR_KEYBOARD_CALLBACKS = +// PREFIX + "STORE_PROJECT_EDITOR_KEYBOARD_CALLBACKS"; -export const STORE_EDITOR_KEYBOARD_CALLBACKS = - PREFIX + "STORE_EDITOR_KEYBOARD_CALLBACKS"; +// export const STORE_EDITOR_KEYBOARD_CALLBACKS = +// PREFIX + "STORE_EDITOR_KEYBOARD_CALLBACKS"; + +export const UPDATE_COUNTER = PREFIX + "UPDATE_COUNTER"; interface StoreProjectEditorKeyboardCallbacks { - type: typeof STORE_PROJECT_EDITOR_KEYBOARD_CALLBACKS; - callbacks: IProjectEditorCallbacks; + type: typeof UPDATE_COUNTER; } export type HotKeysActionTypes = StoreProjectEditorKeyboardCallbacks; diff --git a/src/components/hot-keys/utils.ts b/src/components/hot-keys/utils.ts index 97648e47..c3c58bd1 100644 --- a/src/components/hot-keys/utils.ts +++ b/src/components/hot-keys/utils.ts @@ -3,6 +3,6 @@ import { replace, pipe, when } from "ramda"; export const humanizeKeySequence = (keySequence: string): string => pipe( - when((__) => isMac, replace("command", "⌘")), - when((__) => isMac, replace("opt", "⌥")) + when(() => isMac, replace("command", "⌘")), + when(() => isMac, replace("opt", "⌥")) )(keySequence); diff --git a/src/components/login/actions.tsx b/src/components/login/actions.tsx index 479651a4..941f8953 100644 --- a/src/components/login/actions.tsx +++ b/src/components/login/actions.tsx @@ -1,7 +1,8 @@ -import React, { useState } from "react"; +import React, { useCallback, useState } from "react"; import { doc, getDoc, writeBatch } from "firebase/firestore"; -import TextField from "@material-ui/core/TextField"; -import Button from "@material-ui/core/Button"; +import { useDispatch } from "@root/store"; +import TextField from "@mui/material/TextField"; +import Button from "@mui/material/Button"; import { SET_REQUESTING_STATUS, SIGNIN_FAIL, @@ -26,6 +27,7 @@ import { isEmpty } from "lodash"; import { push } from "connected-react-router"; import { openSnackbar } from "../snackbar/actions"; import { SnackbarType } from "../snackbar/types"; +import { IProfile } from "../profile/types"; export const login = ( email: string, @@ -55,155 +57,170 @@ export const login = ( }; }; -const profileFinalize = ( - user: { displayName: string | undefined; uid: string }, - dispatch: (any) => void -): (() => React.ReactElement) => { - return function ProfileFinalize() { - const [input, setInput] = useState(""); - const [displayName, setDisplayName] = useState(user.displayName || ""); - const [nameReserved, setNameReserved] = useState(false); - const [bio, setBio] = useState(""); - const [link1, setLink1] = useState(""); - const [link2, setLink2] = useState(""); - const [link3, setLink3] = useState(""); +// const profileFinalize = ( +// user: { displayName: string | undefined; uid: string }, +// dispatch: (any) => void +// ): (() => React.ReactElement) => { +// return +// }; - const checkReservedUsername = async (candidate: string) => { - const document_ = await getDoc(doc(usernames, candidate)); - setNameReserved(document_.exists()); - }; +export function ProfileFinalize({ + user +}: { + user: { displayName: string | undefined; uid: string }; +}) { + const dispatch = useDispatch(); + const [input, setInput] = useState(""); + const [displayName, setDisplayName] = useState(user.displayName || ""); + const [nameReserved, setNameReserved] = useState(false); + const [bio, setBio] = useState(""); + const [link1, setLink1] = useState(""); + const [link2, setLink2] = useState(""); + const [link3, setLink3] = useState(""); - const shouldDisable = isEmpty(input) || !/^[\dA-Za-z]+$/.test(input); + const checkReservedUsername = async (candidate: string) => { + const document_ = await getDoc(doc(usernames, candidate)); + setNameReserved(document_.exists()); + }; + + const shouldDisable = isEmpty(input) || !/^[\dA-Za-z]+$/.test(input); + + const handleOnSubmit = useCallback(async () => { + if (!nameReserved) { + const batch = writeBatch(database); - const handleOnSubmit = async () => { - if (!nameReserved) { - const batch = writeBatch(database); + const usernameReference = doc(usernames, input); + const profileReference = doc(profiles, user.uid); + batch.set( + usernameReference, + { userUid: user.uid }, + { merge: true } + ); + batch.set( + profileReference, + { + username: input, + displayName, + bio, + link1, + link2, + link3 + }, + { merge: true } + ); - const usernameReference = doc(usernames, input); - const profileReference = doc(profiles, user.uid); - batch.set( - usernameReference, - { userUid: user.uid }, - { merge: true } + try { + await batch.commit(); + dispatch(closeModal()); + dispatch( + openSnackbar("Profile created!", SnackbarType.Success) ); - batch.set( - profileReference, - { - username: input, - displayName, - bio, - link1, - link2, - link3 - }, - { merge: true } + dispatch({ + type: SIGNIN_SUCCESS, + user + }); + } catch (error) { + dispatch( + openSnackbar( + "Could not create profile: " + error, + SnackbarType.Error + ) ); - - try { - await batch.commit(); - dispatch(closeModal()); - dispatch( - openSnackbar("Profile created!", SnackbarType.Success) - ); - dispatch({ - type: SIGNIN_SUCCESS, - user - }); - } catch (error) { - dispatch( - openSnackbar( - "Could not create profile: " + error, - SnackbarType.Error - ) - ); - } } - }; + } + }, [ + dispatch, + nameReserved, + input, + bio, + displayName, + link1, + link2, + link3, + user + ]); - const textFieldStyle = { marginBottom: 12 }; - return ( -
-

Almost there...

-

- Please choose a unique username and tell us something about - you (if you want to). -
- You can change this data at anytime, except for your - username, so choose it carefully. -

- { - event.target.value.length < 50 && - setInput(event.target.value); - event.target.value.length < 50 && - !isEmpty(event.target.value) && - checkReservedUsername(event.target.value); - }} - error={shouldDisable || nameReserved} - value={input} - /> - { - event.target.value.length < 50 && - setDisplayName(event.target.value || ""); - }} - /> - { - setLink1(event.target.value || ""); - }} - /> - { - setLink2(event.target.value || ""); - }} - /> - { - setLink3(event.target.value || ""); - }} - /> - { - setBio(event.target.value || ""); - }} - minRows={4} - multiline={true} - /> - -
- ); - }; -}; + const textFieldStyle = { marginBottom: 12 }; + return ( +
+

Almost there...

+

+ Please choose a unique username and tell us something about you + (if you want to). +
+ You can change this data at anytime, except for your username, + so choose it carefully. +

+ { + event.target.value.length < 50 && + setInput(event.target.value); + event.target.value.length < 50 && + !isEmpty(event.target.value) && + checkReservedUsername(event.target.value); + }} + error={shouldDisable || nameReserved} + value={input} + /> + { + event.target.value.length < 50 && + setDisplayName(event.target.value || ""); + }} + /> + { + setLink1(event.target.value || ""); + }} + /> + { + setLink2(event.target.value || ""); + }} + /> + { + setLink3(event.target.value || ""); + }} + /> + { + setBio(event.target.value || ""); + }} + minRows={4} + multiline={true} + /> + +
+ ); +} export const thirdPartyAuthSuccess = ( user: { uid: string; displayName: string | undefined }, @@ -233,10 +250,13 @@ export const thirdPartyAuthSuccess = ( if ( profile !== undefined && (!profile.exists || - (profile.data() && isEmpty(profile.data().username))) + (profile.data() && isEmpty(profile.data()!.username))) ) { - const profileFinalizeComp = profileFinalize(user, dispatch); - dispatch(openSimpleModal(profileFinalizeComp, {})); + dispatch( + openSimpleModal("project-finalize", { + user + }) + ); } else { const profileData = profile.data(); @@ -325,10 +345,8 @@ export const setRequestingStatus = ( }; }; -export const resetPassword = ( - email: string -): ((dispatch: any) => Promise) => { - return async (dispatch: any) => { +export const resetPassword = (email: string) => { + return async () => { await sendPasswordResetEmail(getAuth(), email); }; }; diff --git a/src/components/login/login.tsx b/src/components/login/login.tsx index 5b748e4d..e756e519 100644 --- a/src/components/login/login.tsx +++ b/src/components/login/login.tsx @@ -1,5 +1,5 @@ import React, { useState } from "react"; -import { useDispatch, useSelector } from "react-redux"; +import { useDispatch, useSelector } from "@root/store"; import { Button, Dialog, @@ -10,7 +10,7 @@ import { TextField, LinearProgress, Link -} from "@material-ui/core"; +} from "@mui/material"; import { login, closeLoginDialog, @@ -340,12 +340,15 @@ const Login = (): React.ReactElement => { const renderView = (loginMode: LoginMode) => { switch (loginMode) { - case "login": + case "login": { return loginView(); - case "create": + } + case "create": { return signupView(); - case "reset": + } + case "reset": { return resetView(); + } } }; diff --git a/src/components/login/reducer.tsx b/src/components/login/reducer.tsx index 0d8b5e87..347ea1d2 100644 --- a/src/components/login/reducer.tsx +++ b/src/components/login/reducer.tsx @@ -39,49 +39,54 @@ const LoginReducer = ( ): ILoginReducer => { switch (action.type) { case SIGNIN_REQUEST: { - return assoc("requesting", true, state); + return { ...state, requesting: true }; } case SET_REQUESTING_STATUS: { - return assoc("requesting", action.status, state); + return { ...state, requesting: action.status }; } case CREATE_USER_FAIL: case SIGNIN_FAIL: { - return pipe( - assoc("requesting", false), - assoc("authenticated", false), - assoc("failed", true) - )(state); + return { + ...state, + requesting: false, + authenticated: false, + failed: true + }; + } + case CREATE_CLEAR_ERROR: { + const { ...restState } = state; + return { + ...restState, + failed: false, + errorMessage: "" + }; } - case CREATE_CLEAR_ERROR: - return pipe( - assoc("failed", false), - dissoc("errorCode"), - assoc("errorMessage", false) - )(state); case CREATE_USER_SUCCESS: case SIGNIN_SUCCESS: { - return pipe( - assoc("loggedInUid", action.user.uid), - assoc("isLoginDialogOpen", false), - assoc("requesting", false), - assoc("authenticated", true) - )(state); + return { + ...state, + loggedInUid: action.user.uid, + isLoginDialogOpen: false, + requesting: false, + authenticated: true + }; } case LOG_OUT: { - return pipe( - assoc("isLoginDialogOpen", false), - assoc("authenticated", false), - assoc("requesting", false), - assoc("failed", false), - dissoc("errorCode"), - assoc("errorMessage", false) - )(state); + const { ...restState } = state; + return { + ...restState, + isLoginDialogOpen: false, + authenticated: false, + requesting: false, + failed: false, + errorMessage: "" + }; } case OPEN_DIALOG: { - return assoc("isLoginDialogOpen", true, state); + return { ...state, isLoginDialogOpen: true }; } case CLOSE_DIALOG: { - return assoc("isLoginDialogOpen", false, state); + return { ...state, isLoginDialogOpen: false }; } default: { return state; diff --git a/src/components/login/selectors.tsx b/src/components/login/selectors.tsx index 14c72c56..7946c219 100644 --- a/src/components/login/selectors.tsx +++ b/src/components/login/selectors.tsx @@ -1,31 +1,31 @@ -import { IStore } from "@store/types"; +import { RootState } from "@root/store"; -export const selectLoginRequesting = ({ LoginReducer }: IStore): boolean => { +export const selectLoginRequesting = ({ LoginReducer }: RootState): boolean => { return LoginReducer.requesting; }; export const selectErrorCode = ({ LoginReducer -}: IStore): number | undefined => { +}: RootState): number | undefined => { return LoginReducer.errorCode; }; export const selectErrorMessage = ({ LoginReducer -}: IStore): string | undefined => { - return LoginReducer.errorMessaage; +}: RootState): string | undefined => { + return LoginReducer.errorMessage; }; -export const selectLoginFail = ({ LoginReducer }: IStore): boolean => { +export const selectLoginFail = ({ LoginReducer }: RootState): boolean => { return LoginReducer.failed; }; -export const selectAuthenticated = ({ LoginReducer }: IStore): boolean => { +export const selectAuthenticated = ({ LoginReducer }: RootState): boolean => { return LoginReducer.authenticated; }; export const selectLoggedInUid = ({ LoginReducer -}: IStore): string | undefined => { +}: RootState): string | undefined => { return LoginReducer.loggedInUid; }; diff --git a/src/components/login/styles.ts b/src/components/login/styles.ts index ab05a46b..6240e4fb 100644 --- a/src/components/login/styles.ts +++ b/src/components/login/styles.ts @@ -4,7 +4,7 @@ export const errorBox = (theme: Theme): SerializedStyles => css` color: ${theme.errorText}; `; -export const centerLink = (theme: Theme): SerializedStyles => css` +export const centerLink = css` text-align: center; margin-bottom: 10px; `; diff --git a/src/components/login/subscribers.tsx b/src/components/login/subscribers.tsx index 4e62f26a..379e5250 100644 --- a/src/components/login/subscribers.tsx +++ b/src/components/login/subscribers.tsx @@ -1,17 +1,34 @@ import { doc, onSnapshot } from "firebase/firestore"; import { UPDATE_USER_PROFILE } from "./types"; import { profiles } from "@config/firestore"; +import { AppThunkDispatch } from "@root/store"; export const subscribeToLoggedInUserProfile = ( userUid: string, - dispatch: (any) => void + dispatch: AppThunkDispatch ): (() => void) => { const unsubscribe: () => void = onSnapshot( doc(profiles, userUid), (profile) => { + const profileData = profile.data(); + if (!profileData) { + console.error("No profile data found for user", { + userUid, + profile + }); + dispatch({ + type: UPDATE_USER_PROFILE, + profile: undefined, + userUid + }); + return; + } + if (typeof profileData.userJoinDate === "object") { + profileData.userJoinDate = profileData.userJoinDate.toMillis(); + } dispatch({ type: UPDATE_USER_PROFILE, - profile: profile.data(), + profile: profileData, userUid }); }, diff --git a/src/components/main/ios-warning.tsx b/src/components/main/ios-warning.tsx index 3a0f100a..23a57da5 100644 --- a/src/components/main/ios-warning.tsx +++ b/src/components/main/ios-warning.tsx @@ -1,7 +1,7 @@ import { isIOS } from "@root/utils"; import React, { useState } from "react"; -import Dialog from "@material-ui/core/Dialog"; -import Button from "@material-ui/core/Button"; +import Dialog from "@mui/material/Dialog"; +import Button from "@mui/material/Button"; const IosWarning = (): React.ReactElement => { const [open, setOpen] = useState(true); diff --git a/src/components/main/main.test.tsx b/src/components/main/main.test.tsx index dba41e46..ba68380e 100644 --- a/src/components/main/main.test.tsx +++ b/src/components/main/main.test.tsx @@ -2,7 +2,7 @@ import "jest"; import React from "react"; import Main from "./main"; import { TestRenderer } from "react-redux-test-renderer"; -import { configureStore } from "@store/configure-store"; +import { configureStore } from "@root/store"; import { Provider } from "react-redux"; const { store } = configureStore(); @@ -44,12 +44,10 @@ jest.mock("firebase/app", () => { (firestoreMock as any).FieldValue = { serverTimestamp: {} as any }; (firestoreMock as any).FieldPath = { documentId: jest.fn() }; - const authMock = jest - .fn() - .mockReturnValue({ - currentUser: false, - onAuthStateChanged: jest.fn().mockReturnValue(jest.fn()) - }); + const authMock = jest.fn().mockReturnValue({ + currentUser: false, + onAuthStateChanged: jest.fn().mockReturnValue(jest.fn()) + }); (authMock as any).GoogleAuthProvider = { PROVIDER_ID: "" as any }; (authMock as any).FacebookAuthProvider = { PROVIDER_ID: "" as any }; diff --git a/src/components/main/main.tsx b/src/components/main/main.tsx index 98fb709a..43098ee9 100644 --- a/src/components/main/main.tsx +++ b/src/components/main/main.tsx @@ -1,12 +1,8 @@ -import React, { useEffect, useState } from "react"; -import { useDispatch } from "react-redux"; -// import { BrowserRouter } from "react-router-dom"; -// import { ConnectedRouter } from "connected-react-router/esm/index.js"; -// import { history } from "@store"; +import { useEffect, useState } from "react"; +import { useDispatch } from "@root/store"; import { isMobile } from "@root/utils"; -import Router from "@comp/router/router"; +import { WebIdeRouter } from "@comp/router/router"; import ThemeProvider from "@styles/theme-provider"; -import ReactTooltip from "react-tooltip"; import Modal from "@comp/modal"; import IosWarning from "./ios-warning"; import Snackbar from "@comp/snackbar/snackbar"; @@ -16,14 +12,15 @@ import { thirdPartyAuthSuccess } from "@comp/login/actions"; import { getAuth } from "firebase/auth"; +import { ConsoleProvider } from "@comp/console/context"; import HotKeys from "@comp/hot-keys/hot-keys"; -const Main = (): React.ReactElement => { +const Main = () => { const dispatch = useDispatch(); const [autoLoginTimeout, setAutoLoginTimeout] = useState(false); useEffect(() => { - let unsubscribeLoggedInUserProfile; + let unsubscribeLoggedInUserProfile: () => void; // the observer doesn't know if the login state // change is a result of manual login or autologin // we determine this from a timeout @@ -59,22 +56,18 @@ const Main = (): React.ReactElement => { (window as any).ps_body && (window as any).ps_body.destroy(); }; - // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return ( -
- -
- - <> + + - - - + + +
); }; diff --git a/src/components/manual/manual.tsx b/src/components/manual/manual.tsx index 2854a9ef..0ca0bb66 100644 --- a/src/components/manual/manual.tsx +++ b/src/components/manual/manual.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { Link, useParams } from "react-router-dom"; +import { Link, useParams } from "react-router"; import { DebounceInput } from "react-debounce-input"; import { css } from "@emotion/react"; import { doc, getDoc } from "firebase/firestore"; @@ -36,7 +36,9 @@ const formControls = css` border: 1px solid #ccc; border-radius: 4px; box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; + transition: + border-color ease-in-out 0.15s, + box-shadow ease-in-out 0.15s; `; function CsoundManualIndex() { @@ -51,9 +53,9 @@ function CsoundManualIndex() { ); const handleSearch = React.useCallback( - (event) => { - setFilterString(event.target.value); - if (event.target.value) { + (event: React.KeyboardEvent) => { + setFilterString(event.currentTarget.value); + if (event.currentTarget.value) { const fuse = new Fuse(allDocuments, { shouldSort: true, findAllMatches: true, @@ -65,7 +67,7 @@ function CsoundManualIndex() { }); setFilteredDocuments( - fuse.search(event.target.value).map((i) => i.item) + fuse.search(event.currentTarget.value).map((i) => i.item) ); } else { setFilteredDocuments(); @@ -77,6 +79,7 @@ function CsoundManualIndex() { React.useEffect(() => { if (!isMounted) { setIsMounted(true); + document.title = "Csound Manual"; fetch("/static-manual-index.json").then(async (response) => { setAllDocuments((await response.json()) as StaticManualEntry[]); }); @@ -120,7 +123,7 @@ function CsoundManualIndex() { minLength={1} debounceTimeout={300} value={filterString} - onChange={handleSearch} + onChange={handleSearch as any} type="text" className="manual-main-form-control" placeholder="Search by name or description" diff --git a/src/components/menu-bar/menu-bar.tsx b/src/components/menu-bar/menu-bar.tsx index 0406e46c..8eb7a56e 100644 --- a/src/components/menu-bar/menu-bar.tsx +++ b/src/components/menu-bar/menu-bar.tsx @@ -1,9 +1,9 @@ import React, { useState } from "react"; +import { RootState, useDispatch, useSelector } from "@root/store"; import onClickOutside from "react-onclickoutside"; -import { useSelector, useDispatch } from "react-redux"; import { useLocalStorage } from "react-use-storage"; -import SelectedIcon from "@material-ui/icons/DoneSharp"; -import NestedMenuIcon from "@material-ui/icons/ArrowRightSharp"; +import SelectedIcon from "@mui/icons-material/DoneSharp"; +import NestedMenuIcon from "@mui/icons-material/ArrowRightSharp"; import * as SS from "./styles"; import { hr as hrCss } from "@styles/_common"; import { MenuItemDef } from "./types"; @@ -12,17 +12,12 @@ import { invokeHotKeyCallback } from "@comp/hot-keys/actions"; import { BindingsMap } from "@comp/hot-keys/types"; import { humanizeKeySequence } from "@comp/hot-keys/utils"; import { showTargetsConfigDialog } from "@comp/target-controls/actions"; -import { IStore } from "@store/types"; import { exportProject } from "@comp/projects/actions"; import { toggleManualPanel, setFileTreePanelOpen } from "@comp/project-editor/actions"; -import { - renderToDisk, - enableMidiInput, - enableAudioInput -} from "@comp/csound/actions"; +import { renderToDisk } from "@comp/csound/actions"; import { selectCsoundStatus } from "@comp/csound/selectors"; import { selectIsOwner } from "@comp/project-editor/selectors"; import { changeTheme } from "@comp/themes/action"; @@ -39,7 +34,7 @@ import { import { showKeyboardShortcuts } from "@comp/site-documents/actions"; import { openBottomTab } from "@comp/bottom-tabs/actions"; -function MenuBar(): JSX.Element { +export function MenuBar() { const setConsole = useSetConsole(); const activeProjectUid: string = useSelector( @@ -47,7 +42,7 @@ function MenuBar(): JSX.Element { ); const dispatch = useDispatch(); - const isOwner = useSelector(selectIsOwner(activeProjectUid)); + const isOwner = useSelector(selectIsOwner); const csoundStatus = useSelector(selectCsoundStatus); const keyBindings: BindingsMap | undefined = useSelector( path(["HotKeysReducer", "bindings"]) @@ -61,19 +56,19 @@ function MenuBar(): JSX.Element { path(["ProjectEditorReducer", "manualVisible"], store) ); - const isConsoleVisible = useSelector((store: IStore) => + const isConsoleVisible = useSelector((store: RootState) => (store.BottomTabsReducer.openTabs || []).includes("console") ); const isFileTreeVisible = useSelector( - (store: IStore) => store.ProjectEditorReducer.fileTreeVisible + (store: RootState) => store.ProjectEditorReducer.fileTreeVisible ); - const isSpectralAnalyzerVisible = useSelector((store: IStore) => + const isSpectralAnalyzerVisible = useSelector((store: RootState) => store.BottomTabsReducer.openTabs.includes("spectralAnalyzer") ); - const isMidiPianoVisible = useSelector((store: IStore) => + const isMidiPianoVisible = useSelector((store: RootState) => store.BottomTabsReducer.openTabs.includes("piano") ); @@ -214,21 +209,21 @@ function MenuBar(): JSX.Element { { label: "I/O", submenu: [ - { - label: "Refresh MIDI Input", - callback: () => { - dispatch(enableMidiInput()); - } - }, - { - label: "Refresh Audio Input", - callback: () => { - dispatch(enableAudioInput()); - } - }, - { - seperator: true - }, + // { + // label: "Refresh MIDI Input", + // callback: () => { + // dispatch(enableMidiInput()); + // } + // }, + // { + // label: "Refresh Audio Input", + // callback: () => { + // dispatch(enableAudioInput()); + // } + // }, + // { + // seperator: true + // }, { label: "Enable SharedArrayBuffer", checked: isSabEnabled === "true", @@ -278,7 +273,7 @@ function MenuBar(): JSX.Element { } ]; - (MenuBar as any).handleClickOutside = (event_) => { + (MenuBar as any).handleClickOutside = () => { setOpenPath([]); }; @@ -286,12 +281,16 @@ function MenuBar(): JSX.Element { [] as number[] ); - const reduceRow = (items, openPath: number[], rowNesting: number[]) => + const reduceRow = ( + items: MenuItemDef[], + openPath: number[], + rowNesting: number[] + ) => reduce( (accumulator: React.ReactNode[], item: MenuItemDef) => { const index = accumulator.length; const thisRowNesting = append(index, rowNesting); - const hasChild: boolean = typeof item.submenu !== "undefined"; + const hasChild: boolean = item.submenu !== undefined; if (item.seperator) { accumulator.push(
); @@ -301,7 +300,7 @@ function MenuBar(): JSX.Element { key={index} onClick={(event) => { if (item.hotKey) { - dispatch(invokeHotKeyCallback(item.hotKey)); + invokeHotKeyCallback(item.hotKey); } else { item.callback && !item.disabled && @@ -345,7 +344,7 @@ function MenuBar(): JSX.Element { }} > {reduceRow( - item.submenu, + item.submenu || [], openPath, thisRowNesting )} @@ -365,7 +364,9 @@ function MenuBar(): JSX.Element {

{item.label}

{item.hotKey && keyBindings && - keyBindings[item.hotKey] && ( + ((keyBindings as any)[ + item.hotKey + ] as any) && ( {humanizeKeySequence( propOr( @@ -389,7 +390,7 @@ function MenuBar(): JSX.Element { items ); - const columns = (openPath) => + const columns = (openPath: number[]) => reduce( (accumulator: React.ReactNode[], item: MenuItemDef) => { const index = accumulator.length; @@ -404,7 +405,7 @@ function MenuBar(): JSX.Element { > {!isEmpty(openPath) && !isEmpty(item.submenu) && - reduceRow(item.submenu, openPath, [index])} + reduceRow(item.submenu || [], openPath, [index])} ); accumulator.push( @@ -440,10 +441,3 @@ function MenuBar(): JSX.Element { ); } - -const clickOutsideConfig = { - excludeScrollbar: true, - handleClickOutside: () => (MenuBar as any).handleClickOutside -}; - -export default onClickOutside(MenuBar, clickOutsideConfig) as any as React.FC; diff --git a/src/components/modal/actions.ts b/src/components/modal/actions.ts index 343677a1..3d5cb6df 100644 --- a/src/components/modal/actions.ts +++ b/src/components/modal/actions.ts @@ -1,34 +1,16 @@ -import React from "react"; - -export const closeModal = (): ((dispatch: any) => Promise) => { - return async (dispatch: any) => { - dispatch({ - type: "MODAL_CLOSE" - }); - }; -}; - -export const setOnCloseModal = ( - onClose: () => void -): ((dispatch: any) => Promise) => { - return async (dispatch: any) => { - dispatch({ - type: "MODAL_SET_ON_CLOSE", - onClose - }); +export const closeModal = () => { + return { + type: "MODAL_CLOSE" }; }; export const openSimpleModal = ( - component: (properties: any) => React.ReactElement, + modalComponentName: string, properties: Record | undefined -): ((dispatch: any) => Promise) => { - return async (dispatch: any) => { - dispatch({ - type: "MODAL_OPEN_SIMPLE", - onClose: () => dispatch({ type: "MODAL_CLOSE" }), - properties, - component - }); +) => { + return { + type: "MODAL_OPEN_SIMPLE", + modalComponentName, + properties }; }; diff --git a/src/components/modal/index.tsx b/src/components/modal/index.tsx index f97eb312..334a64a2 100644 --- a/src/components/modal/index.tsx +++ b/src/components/modal/index.tsx @@ -1,14 +1,26 @@ import React, { RefObject, useEffect, useState, useRef } from "react"; -import { useSelector } from "react-redux"; +import { RootState, useDispatch, useSelector } from "@root/store"; +import { TargetControlsConfigDialog } from "@comp/target-controls/config-dialog"; +import ShareDialog from "@comp/share-dialog"; +import { KeyboardShortcuts } from "@comp/site-documents/keyboard-shortcuts"; +import { + AddDocumentPrompt, + DeleteDocumentPrompt, + NewDocumentPrompt, + NewFolderPrompt +} from "@comp/projects/modals"; +import { CloseUnsavedFilePrompt } from "@comp/project-editor/modals"; +import { ProjectModal } from "@comp/profile/project-modal"; +import { ProfileModal } from "@comp/profile/profile-modal"; +import { DeleteProjectModal } from "@comp/profile/delete-project-modal"; +import { ProfileFinalize } from "@comp/login/actions"; import { always } from "ramda"; import * as SS from "./styles"; -import Modal from "@material-ui/core/Modal"; -import Backdrop from "@material-ui/core/Backdrop"; -import Fade from "@material-ui/core/Fade"; -import { IStore } from "@store/types"; -import ResizeObserver from "resize-observer-polyfill"; +import Modal from "@mui/material/Modal"; +import Fade from "@mui/material/Fade"; +import { closeModal } from "./actions"; -function getModalStyle(width, height) { +function getModalStyle(width: number, height: number) { if (!width || !height) { return {}; } @@ -21,21 +33,22 @@ function getModalStyle(width, height) { }; } -export default function GlobalModal(): React.ReactElement { - const modalReference = useRef() as RefObject; +export default function GlobalModal() { + const modalReference: RefObject = useRef(null); const [[width, height], setDimensions] = useState([0, 0]); + const dispatch = useDispatch(); const isOpen: boolean = useSelector( - (store: IStore) => store.ModalReducer.isOpen + (store: RootState) => store.ModalReducer.isOpen ); - const ModalComponent = useSelector( - (store: IStore) => store.ModalReducer.component + const modalComponentName = useSelector( + (store: RootState) => store.ModalReducer.modalComponentName ); const modalProperties = - useSelector((store: IStore) => store.ModalReducer.properties) || {}; + useSelector((store: RootState) => store.ModalReducer.properties) || {}; - const onClose = useSelector((store: IStore) => store.ModalReducer.onClose); + const onClose = () => dispatch(closeModal()); const updateDimensions = (focus: boolean) => { const element = document.querySelector("#modal-window"); @@ -58,11 +71,11 @@ export default function GlobalModal(): React.ReactElement { } } const resizeObserver = new ResizeObserver(handleResize); - let copiedReference; + let copiedReference: HTMLDivElement | null = null; setTimeout(() => { if (modalReference.current) { resizeObserver.observe( - (modalReference.current as unknown) as Element + modalReference.current as unknown as Element ); copiedReference = modalReference.current; } @@ -77,12 +90,18 @@ export default function GlobalModal(): React.ReactElement { return ( @@ -92,7 +111,42 @@ export default function GlobalModal(): React.ReactElement { ref={modalReference} style={getModalStyle(width, height)} > - {ModalComponent && } + {modalComponentName === "target-controls" && ( + + )} + {modalComponentName === "share-dialog" && ( + + )} + {modalComponentName === "keyboard-shortcuts" && ( + + )} + {modalComponentName === "new-document-prompt" && ( + + )} + {modalComponentName === "delete-document-prompt" && ( + + )} + {modalComponentName === "add-document-prompt" && ( + + )} + {modalComponentName === "new-folder-prompt" && ( + + )} + {modalComponentName === "close-unsaved-prompt" && ( + + )} + {modalComponentName === "new-project-prompt" && ( + + )} + {modalComponentName === "profile-edit-dialog" && ( + + )} + {modalComponentName === "delete-project-prompt" && ( + + )} + {modalComponentName === "project-finalize" && ( + + )}
diff --git a/src/components/modal/reducer.tsx b/src/components/modal/reducer.tsx index 9cf3e20c..61fad6d6 100644 --- a/src/components/modal/reducer.tsx +++ b/src/components/modal/reducer.tsx @@ -1,35 +1,28 @@ -import React from "react"; -import { assoc } from "ramda"; +import { assoc, pipe } from "ramda"; export interface IModalReducer { isOpen: boolean; - component: (properties: any) => React.ReactElement; properties: Record | undefined; title?: string; - onClose?: () => void; + modalComponentName?: string; } -const dummyComp = (): React.ReactElement => ( - <> -
- -); - const initialModalState: IModalReducer = { isOpen: false, - component: dummyComp, properties: undefined }; const ModalReducer = ( - state: IModalReducer, + state: IModalReducer | undefined, action: Record ): IModalReducer => { + if (!state) { + return initialModalState; + } switch (action.type) { case "MODAL_CLOSE": { return { isOpen: false, - component: dummyComp, properties: undefined }; } @@ -37,11 +30,11 @@ const ModalReducer = ( return assoc("onClose", action.onClose, state); } case "MODAL_OPEN_SIMPLE": { - state.isOpen = true; - state.component = action.component; - state.onClose = action.onClose; - state.properties = action.properties; - return { ...state }; + return pipe( + assoc("isOpen", true), + assoc("properties", action.properties), + assoc("modalComponentName", action.modalComponentName) + )(state); } default: { return state || initialModalState; diff --git a/src/components/page-404/page-404.tsx b/src/components/page-404/page-404.tsx index b3dac377..1b1607eb 100644 --- a/src/components/page-404/page-404.tsx +++ b/src/components/page-404/page-404.tsx @@ -1,7 +1,6 @@ -import React from "react"; -import Header from "@comp/header/header"; +import { Header } from "@comp/header/header"; import styled from "@emotion/styled"; -import withStyles from "./styles"; +import { rootStyle } from "./styles"; import CSLogo from "../cs-logo/cs-logo"; import { get } from "lodash"; import { Gradient } from "./gradient"; @@ -46,20 +45,20 @@ const MessageText = styled.div` line-height: 0; `; -const Page404 = (properties) => { - const { classes } = properties; - +export const Page404 = (properties: any) => { const message = get(properties, "location.state.message") || "Page Not Found"; return (
@@ -75,5 +74,3 @@ const Page404 = (properties) => {
); }; - -export default withStyles(Page404); diff --git a/src/components/page-404/styles.ts b/src/components/page-404/styles.ts index d0c83b66..7c9356ea 100644 --- a/src/components/page-404/styles.ts +++ b/src/components/page-404/styles.ts @@ -1,43 +1,35 @@ -import { Theme } from "@material-ui/core"; -import { createStyles, withStyles } from "@material-ui/styles"; +import { css } from "@emotion/react"; -const profileStyles = (theme: Theme) => - createStyles({ - root: { - backgroundColor: "#e8e8e8", - fontFamily: "'Space Mono', monospace", - width: "100%", - height: "auto", - bottom: "0px", - top: "0px", - left: 0, - position: "absolute" - }, - centerBox: { - position: "absolute", - width: "600px", - height: "50px", - top: "120px", - left: "50%", - marginTop: "-25px", - marginLeft: "-50px" - }, - startCodingButton: { - fontSize: "22px", - border: "4px solid #518C82", - borderRadius: "80%", - display: "flex", - alignItems: "center", - justifyContent: "center", - width: "220px", - height: "220px", - textDecoration: "none", - background: "#00DFCB" - } - }); +export const rootStyle = css({ + backgroundColor: "#e8e8e8", + fontFamily: "'Space Mono', monospace", + width: "100%", + height: "auto", + bottom: "0px", + top: "0px", + left: 0, + position: "absolute" +}); -// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types -const withStyles_ = (ClassComponent: any) => - withStyles(profileStyles)(ClassComponent); +export const centerBoxStyle = css({ + position: "absolute", + width: "600px", + height: "50px", + top: "120px", + left: "50%", + marginTop: "-25px", + marginLeft: "-50px" +}); -export default withStyles_; +export const startCodingButtonStyle = css({ + fontSize: "22px", + border: "4px solid #518C82", + borderRadius: "80%", + display: "flex", + alignItems: "center", + justifyContent: "center", + width: "220px", + height: "220px", + textDecoration: "none", + background: "#00DFCB" +}); diff --git a/src/components/profile/actions.tsx b/src/components/profile/actions.tsx index 424a7191..a70f6e83 100644 --- a/src/components/profile/actions.tsx +++ b/src/components/profile/actions.tsx @@ -1,6 +1,4 @@ -import { ThunkAction } from "redux-thunk"; -import { Action } from "redux"; -import { IStore } from "@store/types"; +import { AppThunkDispatch, RootState } from "@root/store"; import { getDownloadURL, uploadBytes } from "firebase/storage"; import { collection, @@ -35,7 +33,6 @@ import { IProfile, ProfileActionTypes, ADD_USER_PROJECT, - DELETE_USER_PROJECT, STORE_USER_PROFILE, STORE_PROFILE_PROJECTS_COUNT, STORE_PROFILE_STARS, @@ -43,17 +40,17 @@ import { SET_TAGS_INPUT, GET_ALL_TAGS, SET_CURRENTLY_PLAYING_PROJECT, - CLOSE_CURRENTLY_PLAYING_PROJECT, REFRESH_USER_PROFILE, SET_FOLLOWING_FILTER_STRING, - SET_PROJECT_FILTER_STRING + SET_PROJECT_FILTER_STRING, + SET_FOLLOWING_LOADING, + SET_FOLLOWERS_LOADING, + SET_STARS_LOADING } from "./types"; -import { defaultCsd, defaultOrc, defaultSco } from "@root/templates"; +import { defaultCsd, defaultOrc, defaultSco } from "@root/csound-templates"; import { openSnackbar } from "@comp/snackbar/actions"; import { SnackbarType } from "@comp/snackbar/types"; import { openSimpleModal } from "@comp/modal/actions"; -import { ProjectModal } from "./project-modal"; -import { getDeleteProjectModal } from "./delete-project-modal"; import { selectLoggedInUid } from "@comp/login/selectors"; import { selectCurrentlyPlayingProject } from "./selectors"; import { @@ -61,23 +58,14 @@ import { downloadProjectOnce } from "@comp/projects/actions"; import { getProjectLastModifiedOnce } from "@comp/project-last-modified/actions"; -import { getPlayActionFromProject } from "@comp/target-controls/utils"; +import { + getPlayActionFromProject, + getPlayActionFromTarget +} from "@comp/target-controls/utils"; import { downloadTargetsOnce } from "@comp/target-controls/actions"; import { IProject } from "@comp/projects/types"; -import { fetchCsound, newCsound } from "@comp/csound/actions"; -import { ProfileModal } from "./profile-modal"; -import { - assoc, - concat, - difference, - equals, - keys, - reject, - path, - pathOr, - hasPath, - reduce -} from "ramda"; +import { unsetProject } from "@comp/projects/actions"; +import { assoc, difference, keys, path, hasPath } from "ramda"; const addUserProjectAction = (): ProfileActionTypes => { return { @@ -85,21 +73,24 @@ const addUserProjectAction = (): ProfileActionTypes => { }; }; -const handleProjectTags = async (projectUid, loggedInUserUid, currentTags) => { +const handleProjectTags = async ( + projectUid: string, + loggedInUserUid: string, + currentTags: string[] +) => { const currentProjTagsReference = await getDocs( query(tags, where(projectUid, "==", loggedInUserUid)) ); - /* eslint-disable-next-line unicorn/prefer-object-from-entries */ const currentProjTags = currentProjTagsReference.docs.reduce( (accumulator, document_) => assoc(document_.id, document_.data(), accumulator), {} ); - const newTags = reject( - (t) => keys(currentProjTags).includes(t), - currentTags + const newTags = currentTags.filter( + (t) => !Object.keys(currentProjTags).includes(t) ); + const deletedTags = difference( keys(currentProjTags).sort(), currentTags.sort() @@ -144,8 +135,8 @@ export const addUserProject = iconName: string, iconForegroundColor: string, iconBackgroundColor: string - ): ThunkAction> => - async (dispatch, getState) => { + ) => + async (dispatch: AppThunkDispatch, getState: () => RootState) => { const currentState = getState(); const loggedInUserUid = selectLoggedInUid(currentState); if (loggedInUserUid) { @@ -219,8 +210,8 @@ export const editUserProject = iconName: string, iconForegroundColor: string, iconBackgroundColor: string - ): ThunkAction> => - async (dispatch, getState) => { + ) => + async (dispatch: any, getState: () => RootState) => { const currentState = getState(); const loggedInUserUid = selectLoggedInUid(currentState); if (loggedInUserUid) { @@ -255,30 +246,24 @@ export const editUserProject = } }; -const deleteUserProjectAction = (): ProfileActionTypes => { - return { - type: DELETE_USER_PROJECT - }; -}; - export const deleteUserProject = - (project: IProject): ThunkAction> => - async (dispatch, getState) => { + (projectUid: string) => + async (dispatch: AppThunkDispatch, getState: () => RootState) => { const currentState = getState(); const loggedInUserUid = selectLoggedInUid(currentState); if (loggedInUserUid) { const files = await getDocs( - collection(doc(projects, project.projectUid), "files") + collection(doc(projects, projectUid), "files") ); const batch = writeBatch(database); - const documentReference = doc(projects, project.projectUid); + const documentReference = doc(projects, projectUid); batch.delete(documentReference); files.forEach((d) => batch.delete(d.ref)); try { await batch.commit(); - setTimeout(() => dispatch(deleteUserProjectAction()), 1); - + // Remove the project from local state immediately + dispatch(unsetProject(projectUid)); dispatch(openSnackbar("Project Deleted", SnackbarType.Success)); } catch { dispatch( @@ -302,56 +287,40 @@ export const setTagsInput = (tags: Array): ProfileActionTypes => { }; }; -export const getAllTagsFromUser = - ( - loggedInUserUid: string | undefined, - allUserProjectsUids: string[] - ): ThunkAction> => - async (dispatch, getStore) => { - const store = getStore(); - - const currentAllTags = pathOr( - {}, - ["ProfiledReducer", "profiles", loggedInUserUid, "allTags"], - store - ); +// export const getAllTagsFromUser = +// (loggedInUserUid: string, allUserProjectsUids: string[]) => +// async (dispatch, getStore) => { +// const store: RootState = getStore(); - if (allUserProjectsUids) { - const allTags = reduce( - (accumulator, item) => concat(item.tags || [], accumulator), - [], - allUserProjectsUids - ); - // console.log(); - if (!equals(currentAllTags, allTags)) { - dispatch({ type: GET_ALL_TAGS, allTags, loggedInUserUid }); - } - } - }; +// const currentAllTags = +// store.ProfileReducer.profiles[loggedInUserUid].allTags; + +// if (allUserProjectsUids) { +// const allTags = allUserProjectsUids.reduce((accumulator, item) => +// (item.tags || []).concat(accumulator) +// ); + +// if (!equals(currentAllTags, allTags)) { +// dispatch({ type: GET_ALL_TAGS, allTags, loggedInUserUid }); +// } +// } +// }; export const addProject = () => { - return async (dispatch: (any) => void): Promise => { - dispatch( - openSimpleModal(ProjectModal, { - name: "New Project", - description: "", - label: "Create Project", - newProject: true, - projectID: "", - iconName: undefined, - iconForegroundColor: undefined, - iconBackgroundColor: undefined - }) - ); - }; + return openSimpleModal("new-project-prompt", { + name: "New Project", + description: "", + label: "Create Project", + newProject: true, + projectID: "", + iconName: undefined, + iconForegroundColor: undefined, + iconBackgroundColor: undefined + }); }; export const followUser = - ( - loggedInUserUid: string, - profileUid: string - ): ThunkAction> => - async (dispatch) => { + (loggedInUserUid: string, profileUid: string) => async () => { const batch = writeBatch(database); const followersReference = doc(followers, profileUid); const followersData = await getDoc(followersReference); @@ -382,11 +351,7 @@ export const followUser = }; export const unfollowUser = - ( - loggedInUserUid: string, - profileUid: string - ): ThunkAction> => - async (dispatch) => { + (loggedInUserUid: string, profileUid: string) => async () => { const batch = writeBatch(database); batch.update(doc(followers, profileUid), { [loggedInUserUid]: fieldDelete() @@ -407,8 +372,8 @@ export const updateUserProfile = link2: string, link3: string, backgroundIndex: number - ): ThunkAction> => - async (dispatch, getState) => { + ) => + async (dispatch: AppThunkDispatch, getState: () => RootState) => { const currentState = getState(); const loggedInUserUid = selectLoggedInUid(currentState); if (loggedInUserUid) { @@ -441,7 +406,10 @@ export const editProfile = ( link2: string, link3: string, backgroundIndex: number -): ((dispatch: (any) => void, getState: () => IStore) => Promise) => { +): (( + dispatch: AppThunkDispatch, + getState: () => RootState +) => Promise) => { return async (dispatch, getState) => { const currentState = getState(); const loggedInUserUid = selectLoggedInUid(currentState); @@ -455,7 +423,7 @@ export const editProfile = ( }); dispatch( - openSimpleModal(ProfileModal, { + openSimpleModal("profile-edit-dialog", { existingNames: existingNames, username: username, displayName: displayName, @@ -470,40 +438,29 @@ export const editProfile = ( }; }; -export const editProject = ( - project: IProject -): ((dispatch: any) => Promise) => { - return async (dispatch) => { - dispatch( - openSimpleModal(ProjectModal, { - name: project.name, - description: project.description, - label: "Apply changes", - projectID: project.projectUid, - iconName: project.iconName, - iconForegroundColor: project.iconForegroundColor, - iconBackgroundColor: project.iconBackgroundColor, - newProject: false - }) - ); - }; +export const editProject = (project: IProject) => { + return openSimpleModal("new-project-prompt", { + name: project.name, + description: project.description, + label: "Apply changes", + projectID: project.projectUid, + iconName: project.iconName, + iconForegroundColor: project.iconForegroundColor, + iconBackgroundColor: project.iconBackgroundColor, + newProject: false + }); }; -export const deleteProject = ( - project: IProject -): ((dispatch: any) => Promise) => { - return async (dispatch) => { - const DeleteProjectModal = getDeleteProjectModal(project); - dispatch(openSimpleModal(DeleteProjectModal, {})); - }; +export const deleteProject = (project: IProject) => { + return openSimpleModal("delete-project-prompt", { + projectUid: project.projectUid, + projectName: project.name + }); }; export const uploadProfileImage = - ( - loggedInUserUid: string, - file: File - ): ThunkAction> => - async (dispatch) => { + (loggedInUserUid: string, file: File) => + async (dispatch: AppThunkDispatch) => { try { const uploadStorage = await storageReference( `images/${loggedInUserUid}/profile.jpeg` @@ -528,16 +485,12 @@ export const uploadProfileImage = }; export const playListItem = - ( - projectUid: string | false - ): ((dispatch: (any) => void, getState: () => IStore) => Promise) => - async (dispatch, getState) => { + ({ projectUid }: { projectUid: string | false }) => + async ( + dispatch: AppThunkDispatch, + getState: () => RootState + ): Promise => { const state = getState(); - let Csound = state.csound.factory; - - if (!Csound) { - Csound = await fetchCsound(dispatch); - } const currentlyPlayingProject = selectCurrentlyPlayingProject(state); @@ -547,7 +500,7 @@ export const playListItem = } if (projectUid !== currentlyPlayingProject) { - await dispatch({ + dispatch({ type: SET_CURRENTLY_PLAYING_PROJECT, projectUid: undefined }); @@ -564,20 +517,19 @@ export const playListItem = let timestampMismatch = false; if (projectIsCached && projectHasLastModule) { - const cachedTimestamp: Timestamp | undefined = path( - [ - "ProjectsReducer", - "projects", - projectUid, - "cachedProjectLastModified" - ], - state - ); - const currentTimestamp: Timestamp | undefined = path( - ["ProjectLastModifiedReducer", projectUid, "timestamp"], - state - ); - if (cachedTimestamp && currentTimestamp) { + const cachedTimestamp: Timestamp | number | undefined = + state.ProjectsReducer.projects[projectUid] + .cachedProjectLastModified; + + const currentTimestamp: Timestamp | number | undefined = + state.ProjectLastModifiedReducer[projectUid].timestamp; + + if ( + cachedTimestamp && + currentTimestamp && + typeof cachedTimestamp === "object" && + typeof currentTimestamp === "object" + ) { timestampMismatch = (cachedTimestamp as Timestamp).toMillis() !== (currentTimestamp as Timestamp).toMillis(); @@ -585,32 +537,25 @@ export const playListItem = } if (!projectIsCached || timestampMismatch || !projectHasLastModule) { - await downloadProjectOnce(projectUid)(dispatch); + const result = await downloadProjectOnce(projectUid)(dispatch); + if (!result.exists) { + console.error("Project not found:", projectUid); + return; + } await downloadAllProjectDocumentsOnce(projectUid)(dispatch); await downloadTargetsOnce(projectUid)(dispatch); await getProjectLastModifiedOnce(projectUid)(dispatch); // recursion - return await playListItem(projectUid)(dispatch, getState); + return await playListItem({ projectUid })(dispatch, getState); } - let csound = state.csound.csound; - - if (!csound) { - csound = await newCsound(Csound, dispatch); - } else { - csound && (await csound.terminateInstance()); - csound = await newCsound(Csound, dispatch); - } - - csound && - csound.on("realtimePerformanceEnded", () => - dispatch({ type: CLOSE_CURRENTLY_PLAYING_PROJECT }) - ); - const playAction = getPlayActionFromProject(projectUid, state); + const playAction = + getPlayActionFromTarget(projectUid)(state) || + getPlayActionFromProject(projectUid, state); if (playAction) { - playAction(dispatch); - await dispatch({ + (playAction as any)(dispatch); + dispatch({ type: SET_CURRENTLY_PLAYING_PROJECT, projectUid }); @@ -622,7 +567,7 @@ export const playListItem = export const storeProfileProjectsCount = ( projectsCount: ProjectsCount, profileUid: string -): Record => { +) => { return { type: STORE_PROFILE_PROJECTS_COUNT, projectsCount, @@ -630,10 +575,7 @@ export const storeProfileProjectsCount = ( }; }; -export const storeUserProfile = ( - profile: IProfile, - profileUid: string -): Record => { +export const storeUserProfile = (profile: IProfile, profileUid: string) => { return { type: STORE_USER_PROFILE, profile, @@ -644,7 +586,7 @@ export const storeUserProfile = ( export const storeProfileStars = ( stars: Record, profileUid: string -): Record => { +) => { return { type: STORE_PROFILE_STARS, profileUid, @@ -687,7 +629,14 @@ export const starOrUnstarProject = ( loggedInUserUid ); - if (!currentlyStarred) { + if (currentlyStarred) { + batch.update(doc(stars, projectUid), { + [loggedInUserUid]: fieldDelete() + }); + batch.update(doc(profileStars, loggedInUserUid), { + [projectUid]: fieldDelete() + }); + } else { batch.set( doc(stars, projectUid), { [loggedInUserUid]: getFirebaseTimestamp() }, @@ -698,14 +647,32 @@ export const starOrUnstarProject = ( { [projectUid]: getFirebaseTimestamp() }, { merge: true } ); - } else { - batch.update(doc(stars, projectUid), { - [loggedInUserUid]: fieldDelete() - }); - batch.update(doc(profileStars, loggedInUserUid), { - [projectUid]: fieldDelete() - }); } await batch.commit(); }; }; + +// Loading state actions +export const setFollowingLoading = ( + profileUid: string, + isLoading: boolean +) => ({ + type: SET_FOLLOWING_LOADING, + profileUid, + isLoading +}); + +export const setFollowersLoading = ( + profileUid: string, + isLoading: boolean +) => ({ + type: SET_FOLLOWERS_LOADING, + profileUid, + isLoading +}); + +export const setStarsLoading = (profileUid: string, isLoading: boolean) => ({ + type: SET_STARS_LOADING, + profileUid, + isLoading +}); diff --git a/src/components/profile/cached-avatar.tsx b/src/components/profile/cached-avatar.tsx new file mode 100644 index 00000000..6912c37c --- /dev/null +++ b/src/components/profile/cached-avatar.tsx @@ -0,0 +1,99 @@ +import React, { useState, useEffect } from "react"; +import Avatar from "@mui/material/Avatar"; +import { profileImageCache } from "./cached-profile-image"; + +interface CachedAvatarProps { + src?: string; + alt?: string; + className?: string; + style?: React.CSSProperties; + children?: React.ReactNode; + [key: string]: any; // Allow other Avatar props +} + +export const CachedAvatar: React.FC = ({ + src, + alt, + className, + style, + children, + ...otherProps +}) => { + const [cachedSrc, setCachedSrc] = useState(src); + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + if (!src) { + setCachedSrc(undefined); + return; + } + + // Check cache first + const cached = profileImageCache.get(src); + if (cached) { + setCachedSrc(cached); + return; + } + + // If not cached, show loading state and cache the image + setIsLoading(true); + + // Use the original src while we cache it in the background + setCachedSrc(src); + + // Cache the image for future use + const cacheImage = async () => { + try { + const img = new Image(); + img.crossOrigin = "anonymous"; + + img.onload = () => { + try { + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d"); + + if (ctx) { + canvas.width = img.width; + canvas.height = img.height; + ctx.drawImage(img, 0, 0); + + const dataUrl = canvas.toDataURL("image/jpeg", 0.8); + profileImageCache.set(src, dataUrl); + setCachedSrc(dataUrl); + } + } catch (error) { + console.warn("Failed to cache avatar image:", error); + } finally { + setIsLoading(false); + } + }; + + img.onerror = () => { + console.warn("Failed to load avatar image for caching"); + setIsLoading(false); + }; + + img.src = src; + } catch (error) { + console.warn("Failed to cache avatar image:", error); + setIsLoading(false); + } + }; + + cacheImage(); + }, [src]); + + return ( + + {children} + + ); +}; + +export default CachedAvatar; diff --git a/src/components/profile/cached-profile-image.tsx b/src/components/profile/cached-profile-image.tsx new file mode 100644 index 00000000..b1c2d151 --- /dev/null +++ b/src/components/profile/cached-profile-image.tsx @@ -0,0 +1,394 @@ +import React, { useState, useEffect, useRef } from "react"; +import { styled } from "@mui/material/styles"; + +interface CachedImageProps { + src?: string; + alt?: string; + width?: string | number; + height?: string | number; + className?: string; + style?: React.CSSProperties; + onLoad?: () => void; + onError?: () => void; +} + +const StyledImg = styled("img")` + object-fit: cover; +`; + +// Cache configuration +const CACHE_PREFIX = "csound_profile_img_"; +const CACHE_EXPIRY_HOURS = 24; // Cache for 24 hours +const MAX_CACHE_SIZE = 50; // Maximum number of cached images + +interface CacheEntry { + dataUrl: string; + timestamp: number; + lastAccessed: number; +} + +class ImageCache { + private static instance: ImageCache; + + static getInstance(): ImageCache { + if (!ImageCache.instance) { + ImageCache.instance = new ImageCache(); + } + return ImageCache.instance; + } + + private constructor() { + this.cleanupExpiredEntries(); + } + + private getCacheKey(url: string): string { + return CACHE_PREFIX + btoa(url).replace(/[^a-zA-Z0-9]/g, ""); + } + + private isExpired(timestamp: number): boolean { + const now = Date.now(); + const expiryTime = CACHE_EXPIRY_HOURS * 60 * 60 * 1000; + return now - timestamp > expiryTime; + } + + private cleanupExpiredEntries(): void { + try { + const keys = Object.keys(localStorage); + const cacheKeys = keys.filter((key) => + key.startsWith(CACHE_PREFIX) + ); + + for (const key of cacheKeys) { + try { + const entry: CacheEntry = JSON.parse( + localStorage.getItem(key) || "{}" + ); + if (this.isExpired(entry.timestamp)) { + localStorage.removeItem(key); + } + } catch (e) { + // Remove corrupted entries + localStorage.removeItem(key); + } + } + } catch (e) { + console.warn("Failed to cleanup expired cache entries:", e); + } + } + + private enforceMaxCacheSize(): void { + try { + const keys = Object.keys(localStorage); + const cacheKeys = keys.filter((key) => + key.startsWith(CACHE_PREFIX) + ); + + if (cacheKeys.length >= MAX_CACHE_SIZE) { + // Sort by last accessed time and remove oldest entries + const entries = cacheKeys + .map((key) => { + try { + const entry: CacheEntry = JSON.parse( + localStorage.getItem(key) || "{}" + ); + return { + key, + lastAccessed: entry.lastAccessed || 0 + }; + } catch (e) { + return { key, lastAccessed: 0 }; + } + }) + .sort((a, b) => a.lastAccessed - b.lastAccessed); + + // Remove oldest entries to make room + const entriesToRemove = entries.slice( + 0, + cacheKeys.length - MAX_CACHE_SIZE + 1 + ); + for (const entry of entriesToRemove) { + localStorage.removeItem(entry.key); + } + } + } catch (e) { + console.warn("Failed to enforce cache size limit:", e); + } + } + + get(url: string): string | null { + if (!url) return null; + + try { + const key = this.getCacheKey(url); + const cached = localStorage.getItem(key); + + if (cached) { + const entry: CacheEntry = JSON.parse(cached); + + if (this.isExpired(entry.timestamp)) { + localStorage.removeItem(key); + return null; + } + + // Update last accessed time + entry.lastAccessed = Date.now(); + localStorage.setItem(key, JSON.stringify(entry)); + + return entry.dataUrl; + } + } catch (e) { + console.warn("Failed to get cached image:", e); + } + + return null; + } + + async set(url: string, dataUrl: string): Promise { + if (!url || !dataUrl) return; + + try { + this.enforceMaxCacheSize(); + + const key = this.getCacheKey(url); + const entry: CacheEntry = { + dataUrl, + timestamp: Date.now(), + lastAccessed: Date.now() + }; + + localStorage.setItem(key, JSON.stringify(entry)); + } catch (e) { + console.warn("Failed to cache image:", e); + // If localStorage is full, try to clear some space + if (e instanceof DOMException && e.code === 22) { + this.clearOldestEntries(5); + try { + const key = this.getCacheKey(url); + const entry: CacheEntry = { + dataUrl, + timestamp: Date.now(), + lastAccessed: Date.now() + }; + localStorage.setItem(key, JSON.stringify(entry)); + } catch (retryError) { + console.warn( + "Failed to cache image after cleanup:", + retryError + ); + } + } + } + } + + private clearOldestEntries(count: number): void { + try { + const keys = Object.keys(localStorage); + const cacheKeys = keys.filter((key) => + key.startsWith(CACHE_PREFIX) + ); + + const entries = cacheKeys + .map((key) => { + try { + const entry: CacheEntry = JSON.parse( + localStorage.getItem(key) || "{}" + ); + return { key, lastAccessed: entry.lastAccessed || 0 }; + } catch (e) { + return { key, lastAccessed: 0 }; + } + }) + .sort((a, b) => a.lastAccessed - b.lastAccessed); + + for (let i = 0; i < Math.min(count, entries.length); i++) { + localStorage.removeItem(entries[i].key); + } + } catch (e) { + console.warn("Failed to clear oldest cache entries:", e); + } + } + + clear(): void { + try { + const keys = Object.keys(localStorage); + const cacheKeys = keys.filter((key) => + key.startsWith(CACHE_PREFIX) + ); + for (const key of cacheKeys) { + localStorage.removeItem(key); + } + } catch (e) { + console.warn("Failed to clear image cache:", e); + } + } +} + +const imageCache = ImageCache.getInstance(); + +// Convert image URL to data URL for caching +const urlToDataUrl = (url: string): Promise => { + return new Promise((resolve, reject) => { + const img = new Image(); + img.crossOrigin = "anonymous"; + + img.onload = () => { + try { + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d"); + + if (!ctx) { + reject(new Error("Failed to get canvas context")); + return; + } + + canvas.width = img.width; + canvas.height = img.height; + + ctx.drawImage(img, 0, 0); + + // Convert to data URL with compression + const dataUrl = canvas.toDataURL("image/jpeg", 0.8); + resolve(dataUrl); + } catch (error) { + reject(error); + } + }; + + img.onerror = () => { + reject(new Error("Failed to load image")); + }; + + img.src = url; + }); +}; + +export const CachedProfileImage: React.FC = ({ + src, + alt = "Profile Image", + width, + height, + className, + style, + onLoad, + onError +}) => { + const [imageSrc, setImageSrc] = useState(undefined); + const [isLoading, setIsLoading] = useState(false); + const [hasError, setHasError] = useState(false); + const loadingRef = useRef(null); + + useEffect(() => { + if (!src) { + setImageSrc(undefined); + setHasError(false); + return; + } + + // Prevent loading the same image multiple times + if (loadingRef.current === src) { + return; + } + + setHasError(false); + setIsLoading(true); + loadingRef.current = src; + + // Check cache first + const cachedImage = imageCache.get(src); + if (cachedImage) { + setImageSrc(cachedImage); + setIsLoading(false); + loadingRef.current = null; + onLoad?.(); + return; + } + + // Load and cache the image + const loadImage = async () => { + try { + const dataUrl = await urlToDataUrl(src); + + // Only update if this is still the current request + if (loadingRef.current === src) { + await imageCache.set(src, dataUrl); + setImageSrc(dataUrl); + setIsLoading(false); + loadingRef.current = null; + onLoad?.(); + } + } catch (error) { + console.warn("Failed to load and cache image:", error); + + // Only update if this is still the current request + if (loadingRef.current === src) { + // Fallback to original URL + setImageSrc(src); + setIsLoading(false); + setHasError(true); + loadingRef.current = null; + onError?.(); + } + } + }; + + loadImage(); + + return () => { + // Clear loading ref if component unmounts + if (loadingRef.current === src) { + loadingRef.current = null; + } + }; + }, [src, onLoad, onError]); + + if (!src) { + return null; + } + + if (isLoading && !imageSrc) { + // Show a placeholder while loading + return ( +
+
+ Loading... +
+
+ ); + } + + return ( + { + setHasError(true); + onError?.(); + }} + /> + ); +}; + +export default CachedProfileImage; + +// Export cache utilities for manual cache management +export const profileImageCache = { + clear: () => imageCache.clear(), + get: (url: string) => imageCache.get(url), + set: (url: string, dataUrl: string) => imageCache.set(url, dataUrl) +}; diff --git a/src/components/profile/chip-input.tsx b/src/components/profile/chip-input.tsx new file mode 100644 index 00000000..d8cf14b6 --- /dev/null +++ b/src/components/profile/chip-input.tsx @@ -0,0 +1,714 @@ +import React, { useState, useRef, useEffect, useCallback } from "react"; +import Input from "@mui/material/Input"; +import FilledInput from "@mui/material/FilledInput/FilledInput"; +import OutlinedInput from "@mui/material/OutlinedInput"; +import InputLabel from "@mui/material/InputLabel"; +import Chip from "@mui/material/Chip"; +import blue from "@mui/material/colors/blue"; +import FormControl from "@mui/material/FormControl"; +import FormHelperText from "@mui/material/FormHelperText"; +import { css } from "@emotion/react"; + +const variantComponent = { + standard: Input, + filled: FilledInput, + outlined: OutlinedInput +}; + +const inputRootStyle = css({ + display: "inline-flex !important", + flexWrap: "wrap", + flex: 1, + marginTop: 0, + minWidth: 70, + "&$outlined,&$filled": { + boxSizing: "border-box" + }, + "&$outlined": { + paddingTop: 14 + }, + "&$filled": { + paddingTop: 28 + } +}); + +const inputStyle = css({ + textOverflow: "ellipsis", + overflow: "hidden", + whiteSpace: "nowrap", + appearance: "none", + WebkitTapHighlightColor: "rgba(0,0,0,0)", + float: "left", + flex: 1, + display: "inline-flex", + "& input": { + minHeight: 60 + } +}); + +const labelStyle = css({ + top: 4, + "&$outlined&:not($labelShrink)": { + top: 2, + "$marginDense &": { + top: 5 + } + }, + "&$filled&:not($labelShrink)": { + top: 15, + "$marginDense &": { + top: 20 + } + } +}); + +const bottomLineColor = "rgba(255, 255, 255, 0.7)"; + +const underlineStyle = css({ + display: "flex", + "&:after": { + borderBottom: `2px solid white`, + left: 0, + bottom: 0, + content: '""', + position: "absolute", + right: 0, + transform: "scaleX(0)", + pointerEvents: "none" + }, + "&$focused:after": { + transform: "scaleX(1)" + }, + "&$error:after": { + borderBottomColor: "red", + transform: "scaleX(1)" + }, + "&:before": { + borderBottom: `1px solid ${bottomLineColor}`, + left: 0, + bottom: 0, + content: '"\\00a0"', + position: "absolute", + right: 0, + pointerEvents: "none" + }, + "&:hover:not($disabled):not($focused):not($error):before": { + borderBottom: `2px solid gray`, + "@media (hover: none)": { + borderBottom: `1px solid ${bottomLineColor}` + } + }, + "&$disabled:before": { + borderBottomStyle: "dotted" + } +}); + +const keyCodes = { + BACKSPACE: 8, + DELETE: 46, + LEFT_ARROW: 37, + RIGHT_ARROW: 39 +}; + +interface DataSourceConfig { + text: string; + value: string; +} + +interface ChipInputProps { + allowDuplicates?: boolean; + alwaysShowPlaceholder?: boolean; + blurBehavior?: "clear" | "add" | "add-or-clear" | "ignore"; + children?: React.ReactNode; + chipRenderer?: ( + props: ChipRendererProps, + key: React.Key + ) => React.ReactNode; + className?: string; + clearInputValueOnChange?: boolean; + dataSource?: any[]; + dataSourceConfig?: DataSourceConfig; + defaultValue?: any[]; + delayBeforeAdd?: boolean; + disabled?: boolean; + disableUnderline?: boolean; + error?: boolean; + filter?: (searchText: string, key: string) => boolean; + FormHelperTextProps?: any; + fullWidth?: boolean; + fullWidthInput?: boolean; + helperText?: React.ReactNode; + id?: string; + InputProps?: any; + inputRef?: (ref: HTMLInputElement | null) => void; + InputLabelProps?: any; + inputValue?: string; + label?: React.ReactNode; + newChipKeyCodes?: number[]; + newChipKeys?: string[]; + onBeforeAdd?: (chip: any) => boolean; + onAdd?: (chip: any) => void; + onBlur?: (event: React.FocusEvent) => void; + onDelete?: (chip: any, index: number) => void; + onChange?: (chips: any[]) => void; + onFocus?: (event: React.FocusEvent) => void; + onKeyDown?: (event: React.KeyboardEvent) => void; + onKeyPress?: (event: React.KeyboardEvent) => void; + onKeyUp?: (event: React.KeyboardEvent) => void; + onUpdateInput?: (event: React.ChangeEvent) => void; + placeholder?: string; + readOnly?: boolean; + required?: boolean; + rootRef?: (ref: HTMLDivElement | null) => void; + value?: any[]; + variant?: "standard" | "outlined" | "filled"; + [key: string]: any; +} + +interface ChipRendererProps { + value: any; + text: string; + chip: any; + isFocused: boolean; + isDisabled: boolean; + isReadOnly: boolean; + handleClick: () => void; + handleDelete: () => void; + className?: string; +} + +const ChipInput: React.FC = ({ + allowDuplicates = false, + alwaysShowPlaceholder, + blurBehavior = "clear", + children, + chipRenderer = defaultChipRenderer, + className, + clearInputValueOnChange = false, + dataSource, + dataSourceConfig, + defaultValue, + delayBeforeAdd = false, + disabled, + disableUnderline, + error, + filter, + FormHelperTextProps, + fullWidth, + fullWidthInput, + helperText, + id, + InputProps = {}, + inputRef, + InputLabelProps = {}, + inputValue, + label, + newChipKeyCodes = [13], + newChipKeys = ["Enter"], + onBeforeAdd, + onAdd, + onBlur, + onDelete, + onChange, + onFocus, + onKeyDown, + onKeyPress, + onKeyUp, + onUpdateInput, + placeholder, + readOnly, + required, + rootRef, + value, + variant = "standard", + ...other +}) => { + const [chips, setChips] = useState(defaultValue || []); + const [focusedChip, setFocusedChip] = useState(null); + const [inputValueState, setInputValueState] = useState(""); + const [isFocused, setIsFocused] = useState(false); + const [chipsUpdated, setChipsUpdated] = useState(false); + + const labelRef = useRef(null); + const inputRef2 = useRef(null); + const actualInputRef = useRef(null); + const inputBlurTimeoutRef = useRef(null); + const keyPressedRef = useRef(false); + const preventChipCreationRef = useRef(false); + + const actualInputValue = inputValue != null ? inputValue : inputValueState; + + const updateChips = useCallback( + (newChips: any[]) => { + setChips(newChips); + setChipsUpdated(true); + if (onChange) { + onChange(newChips); + } + }, + [onChange] + ); + + const clearInput = useCallback(() => { + setInputValueState(""); + }, []); + + const updateInput = useCallback((newValue: string) => { + setInputValueState(newValue); + }, []); + + const setActualInputRef = useCallback( + (ref: HTMLInputElement | null) => { + actualInputRef.current = ref; + if (inputRef) { + inputRef(ref); + } + }, + [inputRef] + ); + + const handleAddChip = useCallback( + (chip: any, options?: { clearInputOnFail?: boolean }) => { + if (onBeforeAdd && !onBeforeAdd(chip)) { + preventChipCreationRef.current = true; + if (options?.clearInputOnFail) { + clearInput(); + } + return false; + } + clearInput(); + const currentChips = value || chips; + + if (dataSourceConfig) { + if (typeof chip === "string") { + chip = { + [dataSourceConfig.text]: chip, + [dataSourceConfig.value]: chip + }; + } + + if ( + allowDuplicates || + !currentChips.some( + (c: any) => + c[dataSourceConfig.value] === + chip[dataSourceConfig.value] + ) + ) { + if (value && onAdd) { + onAdd(chip); + } else { + updateChips([...chips, chip]); + } + } + return true; + } + + if (chip.trim().length > 0) { + if (allowDuplicates || !currentChips.includes(chip)) { + if (value && onAdd) { + onAdd(chip); + } else { + updateChips([...chips, chip]); + } + } + return true; + } + return false; + }, + [ + onBeforeAdd, + clearInput, + value, + chips, + dataSourceConfig, + allowDuplicates, + onAdd, + updateChips + ] + ); + + const handleDeleteChip = useCallback( + (chip: any, i: number) => { + if (!value) { + const newChips = chips.slice(); + const changed = newChips.splice(i, 1); + if (changed) { + let newFocusedChip = focusedChip; + if (focusedChip === i) { + newFocusedChip = null; + } else if (focusedChip !== null && focusedChip > i) { + newFocusedChip = focusedChip - 1; + } + setFocusedChip(newFocusedChip); + updateChips(newChips); + } + } else if (onDelete) { + onDelete(chip, i); + } + }, + [value, chips, focusedChip, updateChips, onDelete] + ); + + const focus = useCallback(() => { + if (actualInputRef.current) actualInputRef.current.focus(); + if (focusedChip != null) { + setFocusedChip(null); + } + }, [focusedChip]); + + const blur = useCallback(() => { + if (actualInputRef.current) actualInputRef.current.blur(); + }, []); + + const handleInputBlur = useCallback( + (event: React.FocusEvent) => { + if (onBlur) { + onBlur(event); + } + setIsFocused(false); + if (focusedChip != null) { + setFocusedChip(null); + } + const inputValue = event.target.value; + if (blurBehavior === "add" || blurBehavior === "add-or-clear") { + const addChipOptions = + blurBehavior === "add-or-clear" + ? { clearInputOnFail: true } + : undefined; + + if (delayBeforeAdd) { + const numChipsBefore = (value || chips).length; + inputBlurTimeoutRef.current = setTimeout(() => { + const numChipsAfter = (value || chips).length; + if (numChipsBefore === numChipsAfter) { + handleAddChip(inputValue, addChipOptions); + } else { + clearInput(); + } + }, 150); + } else { + handleAddChip(inputValue, addChipOptions); + } + } else if (blurBehavior === "clear") { + clearInput(); + } + }, + [ + onBlur, + focusedChip, + blurBehavior, + delayBeforeAdd, + value, + chips, + handleAddChip, + clearInput + ] + ); + + const handleInputFocus = useCallback( + (event: React.FocusEvent) => { + setIsFocused(true); + if (onFocus) { + onFocus(event); + } + }, + [onFocus] + ); + + const handleKeyDown = useCallback( + (event: React.KeyboardEvent) => { + keyPressedRef.current = false; + preventChipCreationRef.current = false; + if (onKeyDown) { + onKeyDown(event); + if (event.isDefaultPrevented()) { + return; + } + } + const currentChips = value || chips; + if ( + newChipKeyCodes.includes(event.keyCode) || + newChipKeys.includes(event.key) + ) { + const result = handleAddChip( + (event.target as HTMLInputElement).value + ); + if (result !== false) { + event.preventDefault(); + } + return; + } + + switch (event.keyCode) { + case keyCodes.BACKSPACE: + if ((event.target as HTMLInputElement).value === "") { + if (focusedChip != null) { + handleDeleteChip( + currentChips[focusedChip], + focusedChip + ); + if (focusedChip > 0) { + setFocusedChip(focusedChip - 1); + } + } else { + setFocusedChip(currentChips.length - 1); + } + } + break; + case keyCodes.DELETE: + if ( + (event.target as HTMLInputElement).value === "" && + focusedChip != null + ) { + handleDeleteChip( + currentChips[focusedChip], + focusedChip + ); + if (focusedChip <= currentChips.length - 1) { + setFocusedChip(focusedChip); + } + } + break; + case keyCodes.LEFT_ARROW: + if ( + focusedChip == null && + (event.target as HTMLInputElement).value === "" && + currentChips.length + ) { + setFocusedChip(currentChips.length - 1); + } else if (focusedChip != null && focusedChip > 0) { + setFocusedChip(focusedChip - 1); + } + break; + case keyCodes.RIGHT_ARROW: + if ( + focusedChip != null && + focusedChip < currentChips.length - 1 + ) { + setFocusedChip(focusedChip + 1); + } else { + setFocusedChip(null); + } + break; + default: + setFocusedChip(null); + break; + } + }, + [ + onKeyDown, + value, + chips, + newChipKeyCodes, + newChipKeys, + handleAddChip, + focusedChip, + handleDeleteChip + ] + ); + + const handleKeyUp = useCallback( + (event: React.KeyboardEvent) => { + if ( + !preventChipCreationRef.current && + (newChipKeyCodes.includes(event.keyCode) || + newChipKeys.includes(event.key)) && + keyPressedRef.current + ) { + clearInput(); + } else { + updateInput((event.target as HTMLInputElement).value); + } + if (onKeyUp) { + onKeyUp(event); + } + }, + [newChipKeyCodes, newChipKeys, clearInput, updateInput, onKeyUp] + ); + + const handleKeyPress = useCallback( + (event: React.KeyboardEvent) => { + keyPressedRef.current = true; + if (onKeyPress) { + onKeyPress(event); + } + }, + [onKeyPress] + ); + + const handleUpdateInput = useCallback( + (e: React.ChangeEvent) => { + if (inputValue == null) { + updateInput(e.target.value); + } + + if (onUpdateInput) { + onUpdateInput(e); + } + }, + [inputValue, updateInput, onUpdateInput] + ); + + useEffect(() => { + if (value && value.length !== chips.length && clearInputValueOnChange) { + setInputValueState(""); + } + }, [value, chips.length, clearInputValueOnChange]); + + useEffect(() => { + if (disabled) { + setFocusedChip(null); + } + }, [disabled]); + + useEffect(() => { + if (!chipsUpdated && defaultValue) { + setChips(defaultValue); + } + }, [defaultValue, chipsUpdated]); + + useEffect(() => { + return () => { + if (inputBlurTimeoutRef.current) { + clearTimeout(inputBlurTimeoutRef.current); + } + }; + }, []); + + const currentChips = value || chips; + const hasInput = + (value || actualInputValue).length > 0 || actualInputValue.length > 0; + const shrinkFloatingLabel = + InputLabelProps.shrink != null + ? InputLabelProps.shrink + : label != null && + (hasInput || isFocused || currentChips.length > 0); + + const chipComponents = currentChips.map((chip: any, i: number) => { + const chipValue = dataSourceConfig + ? chip[dataSourceConfig.value] + : chip; + return chipRenderer( + { + value: chipValue, + text: dataSourceConfig ? chip[dataSourceConfig.text] : chip, + chip, + isDisabled: !!disabled, + isReadOnly: !!readOnly, + isFocused: focusedChip === i, + handleClick: () => setFocusedChip(i), + handleDelete: () => handleDeleteChip(chip, i) + }, + i + ); + }); + + const InputMore: any = {}; + if (variant === "outlined") { + InputMore.notched = shrinkFloatingLabel; + } + + if (variant !== "standard") { + InputMore.startAdornment = ( + {chipComponents} + ); + } else { + InputProps.disableUnderline = true; + } + + const InputComponent = variantComponent[variant]; + + return ( + + {label && ( + + {label} + + )} +
+ {variant === "standard" && chipComponents} + +
+ {helperText && ( + + {helperText} + + )} +
+ ); +}; + +export const defaultChipRenderer = ( + { + value, + text, + isFocused, + isDisabled, + isReadOnly, + handleClick, + handleDelete, + className + }: ChipRendererProps, + key: React.Key +) => ( + +); + +export default ChipInput; diff --git a/src/components/profile/delete-project-modal.tsx b/src/components/profile/delete-project-modal.tsx index f7f74979..140f351c 100644 --- a/src/components/profile/delete-project-modal.tsx +++ b/src/components/profile/delete-project-modal.tsx @@ -1,38 +1,40 @@ import React, { useState } from "react"; +import { useDispatch } from "@root/store"; import { closeModal } from "@comp/modal/actions"; -import { TextField, Button } from "@material-ui/core"; -import { useDispatch } from "react-redux"; +import { TextField, Button } from "@mui/material"; import { deleteUserProject } from "./actions"; -import { IProject } from "@comp/projects/types"; -export const getDeleteProjectModal = ( - project: IProject -): (() => React.ReactElement) => - function DeleteProjectModal() { - const [name, setName] = useState(""); - const dispatch = useDispatch(); - return ( -
-

Confirm Project Delete

- { - setName(event.target.value); - }} - /> - -
- ); - }; +export function DeleteProjectModal({ + projectUid, + projectName +}: { + projectUid: string; + projectName: string; +}) { + const [name, setName] = useState(""); + const dispatch = useDispatch(); + return ( +
+

Confirm Project Delete

+ { + setName(event.target.value); + }} + /> + +
+ ); +} diff --git a/src/components/profile/list-play-button.tsx b/src/components/profile/list-play-button.tsx index dd6960fa..9219d950 100644 --- a/src/components/profile/list-play-button.tsx +++ b/src/components/profile/list-play-button.tsx @@ -1,12 +1,11 @@ -import React, { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; +import { useDispatch, useSelector } from "@root/store"; import { playListItem } from "./actions"; import { selectCsoundStatus } from "@comp/csound/selectors"; import { pauseCsound, resumePausedCsound } from "@comp/csound/actions"; import { selectCurrentlyPlayingProject } from "./selectors"; -import { useDispatch, useSelector } from "react-redux"; -import AlertIcon from "@material-ui/icons/ErrorOutline"; +import AlertIcon from "@mui/icons-material/ErrorOutline"; import { Theme, useTheme } from "@emotion/react"; -import { IProject } from "@comp/projects/types"; import ProjectAvatar from "@elem/project-avatar"; import * as SS from "./styles"; @@ -39,15 +38,20 @@ const SvgPlayIcon = ({ ); }; -const ListPlayButton = ({ - project +export const ListPlayButton = ({ + projectUid, + iconName, + iconBackgroundColor, + iconForegroundColor }: { - project: IProject; -}): React.ReactElement => { + projectUid: string; + iconName?: string; + iconBackgroundColor?: string; + iconForegroundColor?: string; +}) => { const theme: any = useTheme(); const currentlyPlayingProject = useSelector(selectCurrentlyPlayingProject); const csoundStatus = useSelector(selectCsoundStatus); - const { projectUid, iconBackgroundColor = "#000" } = project; const isPlaying = currentlyPlayingProject === projectUid; const hasError = isPlaying && csoundStatus === "error"; @@ -65,24 +69,27 @@ const ListPlayButton = ({ } }, [isPlaying, csoundStatus, currentlyPlayingProject, isStartingUp]); - const buttonCallback = useCallback( - async (event) => { - if (isStartingUp) { - return; - } else if (!isPlaying) { - setIsStartingUp(true); - } + const buttonCallback = useCallback(async () => { + if (isStartingUp) { + return; + } else if (!isPlaying) { + setIsStartingUp(true); + } - isPaused - ? dispatch(resumePausedCsound()) - : isPlaying && !hasError - ? dispatch(pauseCsound()) - : dispatch(playListItem(projectUid)); - }, - [dispatch, hasError, isPaused, isPlaying, isStartingUp, projectUid] - ); + isPaused + ? dispatch(resumePausedCsound()) + : isPlaying && !hasError + ? dispatch(pauseCsound()) + : dispatch(playListItem({ projectUid })); + }, [dispatch, hasError, isPaused, isPlaying, isStartingUp, projectUid]); - const IconComponent = ; + const IconComponent = ( + + ); return ( ); }; - -export default React.memo(ListPlayButton); diff --git a/src/components/profile/profile-lists.tsx b/src/components/profile/profile-lists.tsx index d8d9c30e..4fd98ab1 100644 --- a/src/components/profile/profile-lists.tsx +++ b/src/components/profile/profile-lists.tsx @@ -1,16 +1,21 @@ import React from "react"; -import { Link } from "react-router-dom"; -import { List, ListItem, ListItemText } from "@material-ui/core"; -import ReactTooltip from "react-tooltip"; -import FollowingList from "./tabs/following-list"; -import FollowersList from "./tabs/followers-list"; -import StarsList from "./tabs/stars-list"; -import ListPlayButton from "./list-play-button"; -import SettingsIcon from "@material-ui/icons/Settings"; -import DeleteIcon from "@material-ui/icons/DeleteOutline"; -import VisibilityIcon from "@material-ui/icons/Visibility"; -import VisibilityOffIcon from "@material-ui/icons/VisibilityOff"; -import { useDispatch, useSelector } from "react-redux"; +import { Link } from "react-router"; +import { useDispatch, useSelector } from "@root/store"; +import { List, ListItem, ListItemText } from "@mui/material"; +import { + selectFollowingLoading, + selectFollowersLoading, + selectStarsLoading +} from "./selectors"; +import { FollowingList } from "./tabs/following-list"; +import { FollowersList } from "./tabs/followers-list"; +import { StarsList } from "./tabs/stars-list"; +import { ListPlayButton } from "./list-play-button"; +import SettingsIcon from "@mui/icons-material/Settings"; +import DeleteIcon from "@mui/icons-material/DeleteOutline"; +import VisibilityIcon from "@mui/icons-material/Visibility"; +import VisibilityOffIcon from "@mui/icons-material/VisibilityOff"; +import Tooltip from "@mui/material/Tooltip"; import { StyledListItemContainer, StyledListItemTopRowText, @@ -19,11 +24,6 @@ import { StyledListPlayButtonContainer, StyledListButtonsContainer } from "./profile-ui"; -import { selectCsoundStatus } from "@comp/csound/selectors"; -import { - selectFilteredUserFollowing, - selectFilteredUserFollowers -} from "./selectors"; import { IProject } from "@comp/projects/types"; import { editProject, deleteProject } from "./actions"; import { markProjectPublic } from "@comp/projects/actions"; @@ -32,23 +32,17 @@ import * as SS from "./styles"; const ProjectListItem = ({ isProfileOwner, - project, - username, - csoundStatus + project }: { isProfileOwner: boolean; project: IProject; - username: string; - csoundStatus: string; }) => { - ReactTooltip.rebuild(); const dispatch = useDispatch(); const { isPublic, projectUid, name, description, tags } = project; - return (
- + } - + {isProfileOwner && ( <> -
-
{ - dispatch(editProject(project)); - event.preventDefault(); - event.stopPropagation(); - }} - > - + +
+
{ + dispatch(editProject(project)); + event.preventDefault(); + event.stopPropagation(); + }} + > + +
-
-
-
{ - dispatch(deleteProject(project)); - event.preventDefault(); - event.stopPropagation(); - }} - > - + + +
+
{ + dispatch(deleteProject(project)); + event.preventDefault(); + event.stopPropagation(); + }} + > + +
-
-
+ -
{ - dispatch( - markProjectPublic(projectUid, !isPublic) - ); - event.preventDefault(); - event.stopPropagation(); - }} - > - {isPublic ? ( - - ) : ( - - )} +
+
{ + dispatch( + markProjectPublic(projectUid, !isPublic) + ); + event.preventDefault(); + event.stopPropagation(); + }} + > + {isPublic ? ( + + ) : ( + + )} +
-
+
)}
); }; -const ProfileLists = ({ +export const ProfileLists = ({ profileUid, selectedSection, isProfileOwner, - filteredProjects, - username + filteredProjects }: { profileUid: string; selectedSection: number; isProfileOwner: boolean; filteredProjects: Array; - username: string; -}): React.ReactElement => { - const csoundStatus = useSelector(selectCsoundStatus); - const filteredFollowing = useSelector( - selectFilteredUserFollowing(profileUid) +}) => { + const userFollowing = useSelector( + (state) => + state.ProfileReducer.profiles[profileUid]?.userFollowing ?? [] ); - const filteredFollowers = useSelector( - selectFilteredUserFollowers(profileUid) + + const userFollowers = useSelector((state) => + (state.ProfileReducer.profiles[profileUid]?.followers ?? []).map( + (followerUid) => state.ProfileReducer.profiles[followerUid] + ) ); + // Loading states + const followingLoading = useSelector(selectFollowingLoading(profileUid)); + const followersLoading = useSelector(selectFollowersLoading(profileUid)); + const starsLoading = useSelector(selectStarsLoading(profileUid)); + return ( {selectedSection === 0 && @@ -179,20 +184,24 @@ const ProfileLists = ({ key={project.projectUid} isProfileOwner={isProfileOwner} project={project} - csoundStatus={csoundStatus} - username={username} /> ); })} - {selectedSection === 1 && Array.isArray(filteredFollowing) && ( - + {selectedSection === 1 && ( + + )} + {selectedSection === 2 && ( + )} - {selectedSection === 2 && Array.isArray(filteredFollowers) && ( - + {selectedSection === 3 && ( + )} - {selectedSection === 3 && } ); }; - -export default ProfileLists; diff --git a/src/components/profile/profile-modal.tsx b/src/components/profile/profile-modal.tsx index 36b1e26d..0840abcb 100644 --- a/src/components/profile/profile-modal.tsx +++ b/src/components/profile/profile-modal.tsx @@ -1,13 +1,13 @@ import React, { useState } from "react"; +import styled from "@emotion/styled"; import { openSnackbar } from "../snackbar/actions"; import { SnackbarType } from "../snackbar/types"; import { updateUserProfile } from "./actions"; import { closeModal } from "../modal/actions"; -import { TextField, Button } from "@material-ui/core"; -import { useDispatch } from "react-redux"; +import { TextField, Button } from "@mui/material"; +import { useDispatch } from "@root/store"; import { useTheme } from "@emotion/react"; import * as TargetSS from "@comp/target-controls/styles"; -import styled from "styled-components"; import Select from "react-select"; const ModalContainer = styled.div` @@ -190,23 +190,18 @@ export const ProfileModal = (properties: IProfileModal): React.ReactElement => { options={backgroundOptions as any} styles={ { - control: (provided, state) => TargetSS.control, - container: (provided, state) => + control: () => TargetSS.control, + container: () => TargetSS.dropdownContainer(theme), - valueContainer: (provided, state) => - TargetSS.valueContainer(theme), - groupHeading: (provided, state) => - TargetSS.groupHeading, - placeholder: (provided, state) => - TargetSS.placeholder, - menu: (provided, state) => TargetSS.menu, - menuList: (provided, state) => - TargetSS.menuList(theme), - option: (provided, state) => - TargetSS.menuOption, - indicatorsContainer: (provided, state) => + valueContainer: () => TargetSS.valueContainer, + groupHeading: () => TargetSS.groupHeading, + placeholder: () => TargetSS.placeholder, + menu: () => TargetSS.menu, + menuList: () => TargetSS.menuList(theme), + option: () => TargetSS.menuOption, + indicatorsContainer: () => TargetSS.indicatorContainer(theme), - indicatorSeparator: (provided, state) => + indicatorSeparator: () => TargetSS.indicatorSeparator } as any } diff --git a/src/components/profile/profile-ui.tsx b/src/components/profile/profile-ui.tsx index 29f33fe0..c3b92411 100644 --- a/src/components/profile/profile-ui.tsx +++ b/src/components/profile/profile-ui.tsx @@ -1,37 +1,38 @@ -import Card from "@material-ui/core/Card"; -import Chip from "@material-ui/core/Chip"; +import Card from "@mui/material/Card"; +import Chip from "@mui/material/Chip"; import { isMobile } from "@root/utils"; -import styled from "styled-components"; +import styled from "@emotion/styled"; import { css } from "@emotion/react"; export const ProfileContainer = styled.div` - display: grid; - ${!isMobile() && - `grid-template-columns: 24px 250px 800px; - grid-template-rows: 50px 175px 1fr 70px;`} + ${isMobile() + ? `padding: 12.5vw;` + : `display: grid; + grid-template-columns: 24px 250px 800px; + grid-template-rows: 50px 175px 1fr 70px; + grid-auto-rows: minmax(90px, auto);`} width: 100%; - grid-auto-rows: minmax(90px, auto); `; export const IDContainer = styled(Card)` - && { - grid-row-start: 2; - grid-row-end: 3; - grid-column-start: 2; - grid-column-end: 3; - display: grid; - grid-template-rows: 250px 1fr 100px; - grid-template-columns: 1fr; - z-index: 2; - min-height: 580px; - box-shadow: 0px 0px 4px rgba(0, 0, 0, 0.8); - & > div { - max-width: 250px; - } + grid-row-start: 2; + grid-row-end: 3; + grid-column-start: 2; + grid-column-end: 3; + display: grid; + grid-template-rows: 250px 1fr auto 60px; + grid-template-columns: 1fr; + z-index: 2; + min-height: 420px; + box-shadow: 0px 0px 4px rgba(0, 0, 0, 0.8); + & > div { + max-width: 250px; } + overflow: initial; + height: fit-content; `; export const DescriptionSection = styled.div` - grid-row: 2; + grid-row: ${(properties: { gridRow: string }) => properties.gridRow}; grid-column: 1; padding: 20px; div, @@ -41,10 +42,18 @@ export const DescriptionSection = styled.div` overflow: hidden; text-overflow: ellipsis; } + a > div { + font-weight: 300; + font-size: 14px; + line-height: 32px; + white-space: nowrap; + text-decoration: underline; + } + height: fit-content; `; export const EditProfileButtonSection = styled.div` - grid-row: 3; + grid-row: 4; grid-column: 1; margin: auto; `; @@ -126,12 +135,11 @@ export const ContentSection = styled.div` border-radius: 4px; box-shadow: 0px 0px 4px rgba(0, 0, 0, 0.8); width: 100%; - overflow: hidden; `; export const ContentTabsContainer = styled.div` background-color: rgba(0, 0, 0, 0.2); `; -export const ContentActionsContainer = styled.div` +export const contentActionsStyle = css` width: 100%; display: flex; justify-content: space-between; @@ -140,6 +148,7 @@ export const ContentActionsContainer = styled.div` padding-left: 24px; padding-right: 12px; margin-top: 12px; + margin-bottom: 24px; `; export const ListContainer = styled.div` diff --git a/src/components/profile/profile.tsx b/src/components/profile/profile.tsx index 279a176f..fb446d4c 100644 --- a/src/components/profile/profile.tsx +++ b/src/components/profile/profile.tsx @@ -1,66 +1,59 @@ import { doc, getDoc } from "firebase/firestore"; -import { Path } from "history"; -import ProfileLists from "./profile-lists"; -import React, { useEffect, useState, RefObject } from "react"; -import ReactTooltip from "react-tooltip"; +import { useDispatch, useSelector } from "@root/store"; +import { ProfileLists } from "./profile-lists"; +import { useEffect, createRef, useState, RefObject } from "react"; import { useTheme } from "@emotion/react"; import { isMobile, updateBodyScroller } from "@root/utils"; import { gradient } from "./gradient"; import { usernames } from "@config/firestore"; -import { push } from "connected-react-router"; -import { useDispatch, useSelector } from "react-redux"; -import withStyles, { createButtonAddIcon } from "./styles"; -import Button from "@material-ui/core/Button"; -import AddIcon from "@material-ui/icons/Add"; -import SearchIcon from "@material-ui/icons/Search"; -import TextField from "@material-ui/core/TextField"; -import Header from "../header/header"; -import ResizeObserver from "resize-observer-polyfill"; +import { useLocation, useParams, useNavigate } from "react-router"; +import { createButtonAddIcon } from "./styles"; +import Button from "@mui/material/Button"; +import Typography from "@mui/material/Typography"; +import Tabs from "@mui/material/Tabs"; +import Tab from "@mui/material/Tab"; +import CameraIcon from "@mui/icons-material/CameraAltOutlined"; +import AddIcon from "@mui/icons-material/Add"; +import Box from "@mui/material/Box"; +import SearchIcon from "@mui/icons-material/Search"; +import TextField from "@mui/material/TextField"; +import InputAdornment from "@mui/material/InputAdornment"; +import { Header } from "../header/header"; +import CachedProfileImage from "./cached-profile-image"; import { subscribeToFollowing, subscribeToFollowers, subscribeToProfile, subscribeToProfileStars, - subscribeToProfileProjects + subscribeToProfileProjects, + subscribeToProjectsCount } from "./subscribers"; import { selectLoginRequesting, selectLoggedInUid } from "@comp/login/selectors"; -import { selectCurrentProfileRoute } from "@comp/router/selectors"; import { uploadProfileImage, addProject, - getAllTagsFromUser, - // getTags, editProfile, followUser, unfollowUser, setProjectFilterString - // setFollowingFilterString - // getLoggedInUserStars } from "./actions"; import { selectUserFollowing, selectUserProfile, selectUserImageURL, - selectAllUserProjectUids, - selectFilteredUserProjects, + selectUserProjects, selectProjectFilterString - // selectFollowingFilterString } from "./selectors"; - import { get } from "lodash"; -import { equals } from "ramda"; -import { Typography, Tabs, Tab, InputAdornment } from "@material-ui/core"; -import CameraIcon from "@material-ui/icons/CameraAltOutlined"; import { stopCsound } from "../csound/actions"; import { ProfileContainer, IDContainer, ProfilePictureContainer, ProfilePictureDiv, - ProfilePicture, UploadProfilePicture, UploadProfilePictureText, UploadProfilePictureIcon, @@ -69,13 +62,13 @@ import { NameSection, ContentSection, ContentTabsContainer, - ContentActionsContainer, + contentActionsStyle, ListContainer, EditProfileButtonSection, fabButton } from "./profile-ui"; -const UserLink = ({ link }) => { +const UserLink = ({ link }: { link: string | undefined }) => { return typeof link === "string" ? ( @@ -87,22 +80,17 @@ const UserLink = ({ link }) => { ); }; -const Profile = ({ classes, ...properties }) => { +export const Profile = () => { const [profileUid, setProfileUid]: [string | undefined, any] = useState(); const theme = useTheme(); const dispatch = useDispatch(); - const [username, profileUriPath] = useSelector(selectCurrentProfileRoute); + const navigate = useNavigate(); + + const { username, tab } = useParams(); const profile = useSelector(selectUserProfile(profileUid)); const imageUrl = useSelector(selectUserImageURL(profileUid)); const loggedInUserUid = useSelector(selectLoggedInUid); - const allUserProjectsUids = useSelector( - selectAllUserProjectUids(loggedInUserUid) - ); - const [lastAllUserProjectUids, setLastAllUserProjectUids] = - useState(allUserProjectsUids); - const filteredProjects = useSelector( - selectFilteredUserProjects(profileUid) - ); + const filteredProjects = useSelector(selectUserProjects(profileUid)); // const followingFilterString = useSelector(selectFollowingFilterString); const projectFilterString = useSelector(selectProjectFilterString); const [imageHover, setImageHover] = useState(false); @@ -116,7 +104,7 @@ const Profile = ({ classes, ...properties }) => { : false; const isRequestingLogin = useSelector(selectLoginRequesting); const isProfileOwner = loggedInUserUid === profileUid; - const uploadReference: RefObject = React.createRef(); + const uploadReference: RefObject = createRef(); useEffect(() => { // start at top on init @@ -126,64 +114,41 @@ const Profile = ({ classes, ...properties }) => { }, []); useEffect(() => { - ReactTooltip.rebuild(); if (username) { - if (!profileUriPath && selectedSection !== 0) { + if (!tab && selectedSection !== 0) { setSelectedSection(0); - } else if ( - profileUriPath === "following" && - selectedSection !== 1 - ) { + } else if (tab === "following" && selectedSection !== 1) { setSelectedSection(1); - } else if ( - profileUriPath === "followers" && - selectedSection !== 2 - ) { + } else if (tab === "followers" && selectedSection !== 2) { setSelectedSection(2); - } else if (profileUriPath === "stars" && selectedSection !== 3) { + } else if (tab === "stars" && selectedSection !== 3) { setSelectedSection(3); } } - }, [profileUriPath, selectedSection, username]); - - useEffect(() => { - if ( - loggedInUserUid && - isProfileOwner && - !equals(lastAllUserProjectUids, allUserProjectsUids) - ) { - dispatch(getAllTagsFromUser(loggedInUserUid, allUserProjectsUids)); - setLastAllUserProjectUids(allUserProjectsUids); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [allUserProjectsUids]); + }, [tab, selectedSection, username]); useEffect(() => { if (!isRequestingLogin) { if (!username && !loggedInUserUid) { - dispatch(push("/")); + navigate("/"); } else if (username) { getDoc(doc(usernames, username)).then((userSnap) => { - if (!userSnap.exists()) { - dispatch( - push({ pathname: "/404" } as Path, { - message: "User not found" - }) - ); - } else { + if (userSnap.exists()) { const data = userSnap.data(); data && data.userUid ? setProfileUid(data.userUid) - : dispatch( - push({ pathname: "/404" } as Path, { - message: "User not found" - }) - ); + : navigate("/404", { + state: { message: "User not found" } + }); + } else { + navigate("/404", { + state: { message: "User not found" } + }); } }); } } - }, [dispatch, username, loggedInUserUid, isRequestingLogin, setProfileUid]); + }, [navigate, username, loggedInUserUid, isRequestingLogin, setProfileUid]); useEffect(() => { if (!isRequestingLogin && profileUid) { @@ -196,7 +161,8 @@ const Profile = ({ classes, ...properties }) => { isProfileOwner, dispatch ), - subscribeToProfileStars(profileUid, dispatch) + subscribeToProfileStars(profileUid, dispatch), + subscribeToProjectsCount(profileUid, dispatch) ] as any[]; // make sure the logged in user's following is listed // when viewing another profile, for un/follow state @@ -240,10 +206,16 @@ const Profile = ({ classes, ...properties }) => { backgroundIndex = 0 } = profile || {}; + useEffect(() => { + if (displayName !== undefined && document.title !== displayName) { + document.title = displayName; + } + }, [displayName]); + return ( -
+
-
+ {!isMobile() && ( @@ -253,7 +225,7 @@ const Profile = ({ classes, ...properties }) => { > {imageUrl && ( - { )} - + Bio @@ -311,6 +283,8 @@ const Profile = ({ classes, ...properties }) => { > {profile && profile.bio} + + Links @@ -330,14 +304,15 @@ const Profile = ({ classes, ...properties }) => { aria-label="Add" size="medium" onClick={() => + profile && dispatch( editProfile( profile.username, - displayName, - bio, - link1, - link2, - link3, + displayName || "", + bio || "", + link1 || "", + link2 || "", + link3 || "", backgroundIndex ) ) @@ -347,33 +322,37 @@ const Profile = ({ classes, ...properties }) => { )} - {!isProfileOwner && profileUid && loggedInUserUid && ( - - - - )} + : dispatch( + followUser( + loggedInUserUid, + profileUid + ) + ); + }} + > + {isFollowing + ? "Unfollow" + : "Follow"} + + + )} )} @@ -391,36 +370,30 @@ const Profile = ({ classes, ...properties }) => { { + onChange={(_, index) => { switch (index) { - case 0: - dispatch( - push({ - pathname: `/profile/${username}` - } as Path) - ); + case 0: { + navigate(`/profile/${username}`); break; - case 1: - dispatch( - push({ - pathname: `/profile/${username}/following` - } as Path) + } + case 1: { + navigate( + `/profile/${username}/following` ); break; - case 2: - dispatch( - push({ - pathname: `/profile/${username}/followers` - } as Path) + } + case 2: { + navigate( + `/profile/${username}/followers` ); break; - case 3: - dispatch( - push({ - pathname: `/profile/${username}/stars` - } as Path) + } + case 3: { + navigate( + `/profile/${username}/stars` ); break; + } } }} indicatorColor={"primary"} @@ -432,15 +405,22 @@ const Profile = ({ classes, ...properties }) => { - {selectedSection === 0 && ( @@ -448,9 +428,6 @@ const Profile = ({ classes, ...properties }) => { ) }} - value={projectFilterString} - variant="outlined" - margin="dense" onChange={(event) => { dispatch( setProjectFilterString( @@ -474,7 +451,7 @@ const Profile = ({ classes, ...properties }) => { )} - + {profileUid && username && ( { isProfileOwner={isProfileOwner} selectedSection={selectedSection} filteredProjects={filteredProjects} - username={username} /> )} -
-
+ + ); }; - -export default withStyles(Profile); diff --git a/src/components/profile/project-modal.tsx b/src/components/profile/project-modal.tsx index 3733ea0e..97a25cc9 100644 --- a/src/components/profile/project-modal.tsx +++ b/src/components/profile/project-modal.tsx @@ -1,24 +1,23 @@ import React, { useState } from "react"; -import Tooltip from "@material-ui/core/Tooltip"; +import { useDispatch } from "@root/store"; +import Tooltip from "@mui/material/Tooltip"; import SVGPaths from "@elem/svg-icons"; import ProjectAvatar from "@elem/project-avatar"; import { IProject } from "@comp/projects/types"; -import { addUserProject, editUserProject } from "./actions"; -import { openSnackbar } from "../snackbar/actions"; -import { SnackbarType } from "../snackbar/types"; -import { closeModal } from "../modal/actions"; import { SliderPicker } from "react-color"; -import Radio from "@material-ui/core/Radio"; -import RadioGroup from "@material-ui/core/RadioGroup"; -import FormControlLabel from "@material-ui/core/FormControlLabel"; -import { TextField, Button, Popover, Grid } from "@material-ui/core"; -import { useDispatch } from "react-redux"; +import Radio from "@mui/material/Radio"; +import RadioGroup from "@mui/material/RadioGroup"; +import FormControlLabel from "@mui/material/FormControlLabel"; +import { TextField, Button, Popover, Grid } from "@mui/material"; import { css } from "@emotion/react"; -import styled from "styled-components"; -import IconButton from "@material-ui/core/IconButton"; +import styled from "@emotion/styled"; +import IconButton from "@mui/material/IconButton"; import ReactAutosuggestExample from "./tag-auto-suggest"; -// import { selectTags } from "./selectors"; import { isEmpty } from "ramda"; +import { addUserProject, editUserProject } from "./actions"; +import { openSnackbar } from "../snackbar/actions"; +import { SnackbarType } from "../snackbar/types"; +import { closeModal } from "../modal/actions"; const avatarContainer = css` margin-left: -16px; @@ -86,7 +85,7 @@ interface IProjectModal { newProject: boolean; } -export const ProjectModal = (properties: IProjectModal): React.ReactElement => { +export const ProjectModal = (properties: IProjectModal) => { const [name, setName] = useState(properties.name); const [description, setDescription] = useState(properties.description); const [iconName, setIconName] = useState(properties.iconName); @@ -99,7 +98,9 @@ export const ProjectModal = (properties: IProjectModal): React.ReactElement => { ); const [popupState, setPopupState] = useState(false); - const [anchorElement, setAnchorElement] = useState(); + const [anchorElement, setAnchorElement] = useState( + null as HTMLSpanElement | null + ); const dispatch = useDispatch(); // const currentTags = useSelector(selectTags(properties.projectID)); const [modifiedTags, setModifiedTags] = useState([]); @@ -147,10 +148,12 @@ export const ProjectModal = (properties: IProjectModal): React.ReactElement => { }; const textFieldStyle = { marginBottom: 12, marginRight: 5 }; - const handleProfileDropDown = (event) => { + const handleProfileDropDown = ( + event: React.MouseEvent + ) => { event.preventDefault(); setPopupState(!popupState); - setAnchorElement(event.currentTarget); + setAnchorElement(event.currentTarget as HTMLSpanElement); }; const handlePopoverClose = () => { @@ -196,7 +199,7 @@ export const ProjectModal = (properties: IProjectModal): React.ReactElement => { @@ -209,13 +212,9 @@ export const ProjectModal = (properties: IProjectModal): React.ReactElement => { onClick={handleProfileDropDown} > @@ -235,6 +234,9 @@ export const ProjectModal = (properties: IProjectModal): React.ReactElement => { {Array.isArray(Object.entries(SVGPaths)) && Object.entries(SVGPaths).map( (entry, index) => { + const SvgElem: any = + entry[1] as any; + return ( { handlePopoverClose(); }} > - {"svg @@ -271,7 +272,7 @@ export const ProjectModal = (properties: IProjectModal): React.ReactElement => { ? iconForegroundColor : iconBackgroundColor } - onChangeComplete={(event) => { + onChangeComplete={(event: { hex: string }) => { if (foregroundColor) { setIconForegroundColor(event.hex); } else { diff --git a/src/components/profile/reducer.ts b/src/components/profile/reducer.ts index 92f65812..54cf3d4b 100644 --- a/src/components/profile/reducer.ts +++ b/src/components/profile/reducer.ts @@ -12,20 +12,11 @@ import { STORE_PROFILE_STARS, GET_ALL_TAGS, UPDATE_PROFILE_FOLLOWING, - UPDATE_PROFILE_FOLLOWERS + UPDATE_PROFILE_FOLLOWERS, + SET_FOLLOWING_LOADING, + SET_FOLLOWERS_LOADING, + SET_STARS_LOADING } from "./types"; -import { - assoc, - assocPath, - dissoc, - hasPath, - keys, - mergeAll, - pick, - pipe, - reduce, - sort -} from "ramda"; type ProfileMap = { [profileUid: string]: IProfile }; @@ -36,6 +27,9 @@ export interface IProfileReducer { readonly currentlyPlayingProject: string | undefined; readonly projectFilterString: string; readonly followingFilterString: string; + readonly followingLoading: { [profileUid: string]: boolean }; + readonly followersLoading: { [profileUid: string]: boolean }; + readonly starsLoading: { [profileUid: string]: boolean }; } const INITIAL_STATE: IProfileReducer = { @@ -44,7 +38,10 @@ const INITIAL_STATE: IProfileReducer = { tagsInput: [], currentlyPlayingProject: undefined, projectFilterString: "", - followingFilterString: "" + followingFilterString: "", + followingLoading: {}, + followersLoading: {}, + starsLoading: {} }; const profileKeys = [ @@ -65,104 +62,164 @@ const ProfileReducer = ( ): IProfileReducer => { switch (action.type) { case SET_FOLLOWING_FILTER_STRING: { - return assoc("followingFilterString", action.payload, state); + return { ...state, followingFilterString: action.payload }; } case SET_PROJECT_FILTER_STRING: { - return assoc("projectFilterString", action.payload, state); + return { ...state, projectFilterString: action.payload }; } case UPDATE_USER_PROFILE: { - return assocPath( - ["profiles", (action as any).userUid], - mergeAll([ - state.profiles[(action as any).userUid] || {}, - pick(profileKeys, action.profile || {}) - ]), - state - ); + return { + ...state, + profiles: { + ...state.profiles, + [(action as any).userUid]: { + ...(state.profiles[(action as any).userUid] || {}), + ...Object.fromEntries( + Object.entries(action.profile || {}).filter( + ([key]) => profileKeys.includes(key) + ) + ) + } + } + }; } case STORE_USER_PROFILE: { - return !hasPath(["profiles", (action as any).profileUid], state) - ? assocPath( - ["profiles", (action as any).profileUid], - action.profile, - state - ) - : state; + const userUid = (action as any).profileUid; + return state.profiles[userUid] + ? state + : { + ...state, + profiles: { + ...state.profiles, + [userUid]: action.profile + } + }; } case GET_ALL_TAGS: { - return assocPath( - ["profiles", action.loggedInUserUid, "allTags"], - action.allTags, - state - ); + return { + ...state, + profiles: { + ...state.profiles, + [action.loggedInUserUid]: { + ...state.profiles[action.loggedInUserUid], + allTags: action.allTags + } + } + }; } case STORE_PROFILE_PROJECTS_COUNT: { - return assocPath( - ["profiles", action.profileUid, "projectsCount"], - action.projectsCount, - state - ); + return { + ...state, + profiles: { + ...state.profiles, + [action.profileUid]: { + ...state.profiles[action.profileUid], + projectsCount: action.projectsCount + } + } + }; } case STORE_PROFILE_STARS: { - const sortedStars = sort( - (x) => x.timestamp, - keys(action.stars).map((p) => ({ - timestamp: action.stars[p].toDate, + const sortedStars = Object.keys(action.stars) + .map((p) => ({ + timestamp: + typeof action.stars[p].toDate === "function" + ? action.stars[p].toDate() + : action.stars[p].toDate, projectUid: p })) - ); - return assocPath( - ["profiles", action.profileUid, "stars"], - sortedStars.map((x) => x.projectUid), - state - ); + .sort((x, y) => x.timestamp - y.timestamp); + + return { + ...state, + profiles: { + ...state.profiles, + [action.profileUid]: { + ...state.profiles[action.profileUid], + stars: sortedStars.map((x) => x.projectUid) + } + } + }; } case UPDATE_PROFILE_FOLLOWING: { - return pipe( - assoc( - "profiles", - reduce( - (accumulator, item) => - assoc(item.userUid, item, accumulator), - state.profiles, - action.userProfiles - ) - ), - assocPath( - ["profiles", action.profileUid, "following"], - (action as any).userProfileUids - ) - )(state); + const updatedProfiles = action.userProfiles.reduce( + (accumulator: any, item: any) => ({ + ...accumulator, + [item.userUid]: item + }), + state.profiles + ); + + return { + ...state, + profiles: { + ...updatedProfiles, + [action.profileUid]: { + ...updatedProfiles[action.profileUid], + following: (action as any).userProfileUids + } + } + }; } case UPDATE_PROFILE_FOLLOWERS: { - return pipe( - assoc( - "profiles", - reduce( - (accumulator, item) => - assoc(item.userUid, item, accumulator), - state.profiles, - action.userProfiles - ) - ), - assocPath( - ["profiles", action.profileUid, "followers"], - (action as any).userProfileUids - ) - )(state); + const updatedProfiles = action.userProfiles.reduce( + (accumulator: any, item: any) => ({ + ...accumulator, + [item.userUid]: item + }), + state.profiles + ); + + return { + ...state, + profiles: { + ...updatedProfiles, + [action.profileUid]: { + ...updatedProfiles[action.profileUid], + followers: (action as any).userProfileUids + } + } + }; } case SET_CURRENTLY_PLAYING_PROJECT: { - return assoc("currentlyPlayingProject", action.projectUid, state); + return { ...state, currentlyPlayingProject: action.projectUid }; } case CLOSE_CURRENTLY_PLAYING_PROJECT: { - return dissoc("currentlyPlayingProject", state); + return { ...state, currentlyPlayingProject: undefined }; } case SET_CSOUND_PLAY_STATE: { if (state.currentlyPlayingProject && action.status === "stopped") { - return dissoc("currentlyPlayingProject", state); + return { ...state, currentlyPlayingProject: undefined }; } return state; } + case SET_FOLLOWING_LOADING: { + return { + ...state, + followingLoading: { + ...state.followingLoading, + [action.profileUid]: action.isLoading + } + }; + } + case SET_FOLLOWERS_LOADING: { + return { + ...state, + followersLoading: { + ...state.followersLoading, + [action.profileUid]: action.isLoading + } + }; + } + case SET_STARS_LOADING: { + return { + ...state, + starsLoading: { + ...state.starsLoading, + [action.profileUid]: action.isLoading + } + }; + } default: { return state; } diff --git a/src/components/profile/selectors.ts b/src/components/profile/selectors.ts index c406fb3a..ffe0bf7f 100644 --- a/src/components/profile/selectors.ts +++ b/src/components/profile/selectors.ts @@ -1,70 +1,78 @@ -import { IStore } from "@store/types"; +import { RootState } from "@root/store"; import { createSelector } from "reselect"; import { IProfileReducer } from "./reducer"; -// import { IProfile } from "./types"; -import { IFirestoreProfile } from "@db/types"; -import { path, pathOr, pickBy, propEq, values } from "ramda"; -import Fuse from "fuse.js"; - -export const selectUserFollowing = ( - profileUid: string | undefined -): ((store: IStore) => Array) => (store: IStore) => { - if (!profileUid) { - return []; - } else { - const state: IProfileReducer = store.ProfileReducer; - return pathOr([], ["profiles", profileUid, "following"], state); - } -}; +import { path, pathOr } from "ramda"; +// import Fuse from "fuse.js"; +import { IProject } from "../projects/types"; -export const selectUserProjects = ( - profileUid: string | undefined -): ((store: IStore) => Array) => (store: IStore) => { - if (!profileUid) { - return []; - } else { - const state = store.ProjectsReducer.projects; - return values((pickBy as any)(propEq("userUid", profileUid), state)); - } -}; +export const selectUserFollowing = + (profileUid: string | undefined): ((store: RootState) => Array) => + (store: RootState) => { + if (profileUid) { + const state: IProfileReducer = store.ProfileReducer; + return pathOr([], ["profiles", profileUid, "following"], state); + } else { + return []; + } + }; + +export const selectUserProjects = + (profileUid: string | undefined) => (store: RootState) => { + if (profileUid) { + const state = store.ProjectsReducer.projects; + + // Filter projects by matching `userUid` with `profileUid` + const filteredProjects = Object.values(state).filter( + (project) => project.userUid === profileUid + ); + + return filteredProjects; + } else { + return []; + } + }; export const selectProjectFilterString = ( - store: IStore + store: RootState ): string | undefined => { const state: IProfileReducer = store.ProfileReducer; return state.projectFilterString; }; export const selectFollowingFilterString = ( - store: IStore + store: RootState ): string | undefined => { const state: IProfileReducer = store.ProfileReducer; return state.followingFilterString; }; -export const selectUserProfile = ( - profileUid: string | undefined -): ((store: any) => IFirestoreProfile) => (store: any) => { - if (!profileUid) { - return; - } else { - const state: IProfileReducer = store.ProfileReducer; - return path(["profiles", profileUid], state); - } -}; +export const selectUserProfile = + (profileUid: string | undefined) => (store: RootState) => { + if (profileUid) { + const state: IProfileReducer = store.ProfileReducer; + return state.profiles[profileUid]; + } + }; -export const selectUserName = ( - profileUid: string | undefined -): ((store: IStore) => string | undefined) => (store) => { - if (!profileUid) { - return; - } else { - const state: IProfileReducer = store.ProfileReducer; - return path(["profiles", profileUid, "username"], state); +const selectLogInReducer = (state: RootState) => state.LoginReducer; +const selectProfiles = (state: RootState) => state.ProfileReducer.profiles; + +// Selector to get the username for a specific profileUid +export const selectLoggedInUserName = createSelector( + [selectLogInReducer, selectProfiles], + (loggedInUser, profiles) => { + if (!loggedInUser || !loggedInUser.loggedInUid) { + return undefined; + } + + const matchingProfile = profiles[loggedInUser.loggedInUid]; + if (matchingProfile) { + return matchingProfile.username; + } } -}; +); -export const selectLoggedInUserStars = (store: IStore): Array => { +export const selectLoggedInUserStars = (store: RootState): Array => { const loggedInUid: string | undefined = store.LoginReducer.loggedInUid; if (loggedInUid) { const state: IProfileReducer = store.ProfileReducer; @@ -74,157 +82,187 @@ export const selectLoggedInUserStars = (store: IStore): Array => { } }; -export const selectFilteredUserProjects = ( - profileUid: string | undefined -): ((any) => any) => - createSelector( - [ - selectUserProjects(profileUid), - selectProjectFilterString, - selectLoggedInUserStars - ], - (userProjects, projectFilterString, stars) => { - let result: any = []; - if ( - typeof projectFilterString === "undefined" || - projectFilterString === "" - ) { - result = userProjects; - } else { - const options = { - shouldSort: true, - keys: ["description", "name", "tags"] - }; - - const fuse = new Fuse(userProjects, options); - result = fuse.search(projectFilterString).map((x) => x.item); - } +// export const selectFilteredUserProjects = createSelector( +// [selectUserProjects, selectProjectFilterString], +// (userProjects, projectFilterString) => { +// let result: any = []; +// if (projectFilterString === undefined || projectFilterString === "") { +// result = userProjects; +// } else { +// const options = { +// shouldSort: true, +// keys: ["description", "name", "tags"] +// }; - return result; - } - ); - -export const selectFilteredUserFollowing = ( - profileUid: string -): ((store: IStore) => any) => (store) => - createSelector( - [selectUserFollowing(profileUid), selectFollowingFilterString], - (userFollowing, followingFilterString) => { - const followingProfiles = userFollowing.map( - (profileUid) => selectUserProfile(profileUid)(store) || {} - ); +// const fuse = new Fuse(userProjects, options); +// result = fuse.search(projectFilterString).map((x) => x.item); +// } - if ( - typeof followingFilterString === "undefined" || - followingFilterString === "" - ) { - return followingProfiles; - } +// return result; +// } +// ); - const options = { - shouldSort: true, - threshold: 0.4, - location: 0, - distance: 100, - maxPatternLength: 32, - minMatchCharLength: 1, - keys: ["username", "bio", "displayName"] - }; - const fuse = new Fuse(followingProfiles, options); - const result = fuse.search(followingFilterString); - return result; - } - )(store); +// export const selectFilteredUserFollowing = +// (profileUid: string): ((store: RootState) => any) => +// (store) => +// createSelector( +// [selectUserFollowing(profileUid), selectFollowingFilterString], +// (userFollowing, followingFilterString) => { +// const followingProfiles = userFollowing.map( +// (profileUid) => selectUserProfile(profileUid)(store) || {} +// ); -export const selectFilteredUserFollowers = ( - profileUid: string -): ((store: IStore) => any) => (store) => { - const state: IProfileReducer = store.ProfileReducer; - const followerUids = pathOr( - [], - ["profiles", profileUid, "followers"], - state - ); - return (followerUids || []).map((followerUid) => - path(["profiles", followerUid], state) - ); -}; +// if ( +// followingFilterString === undefined || +// followingFilterString === "" +// ) { +// return followingProfiles; +// } -export const selectUserImageURL = ( - profileUid: string | undefined -): ((store: IStore) => any) => (store) => { - if (!profileUid) { - return; - } else { +// const options = { +// shouldSort: true, +// threshold: 0.4, +// location: 0, +// distance: 100, +// maxPatternLength: 32, +// minMatchCharLength: 1, +// keys: ["username", "bio", "displayName"] +// }; +// const fuse = new Fuse(followingProfiles, options); +// const result = fuse.search(followingFilterString); +// return result; +// } +// )(store); + +export const selectFilteredUserFollowers = + (profileUid: string): ((store: RootState) => any) => + (store) => { const state: IProfileReducer = store.ProfileReducer; - return pathOr("", ["profiles", profileUid, "photoUrl"], state); - } -}; + const followerUids = pathOr( + [], + ["profiles", profileUid, "followers"], + state + ); + return (followerUids || []).map((followerUid) => + path(["profiles", followerUid], state) + ); + }; + +export const selectUserImageURL = + (profileUid: string | undefined) => (store: RootState) => { + if (profileUid) { + const state: IProfileReducer = store.ProfileReducer; + return state.profiles[profileUid]?.photoUrl ?? undefined; + } else { + return; + } + }; export const selectCurrentlyPlayingProject = ( - store: IStore + store: RootState ): string | undefined => { const state: IProfileReducer = store.ProfileReducer; return state.currentlyPlayingProject; }; -export const selectCurrentTagText = (store: IStore): string => { +export const selectCurrentTagText = (store: RootState): string => { const state: IProfileReducer = store.ProfileReducer; return state.currentTagText; }; -export const selectTags = (projectUid: string): ((store: IStore) => any) => ( - store -) => { - return pathOr( - [], - ["ProjectsReducer", "projects", projectUid, "tags"], - store - ); -}; +export const selectTags = + (projectUid: string): ((store: RootState) => any) => + (store) => { + return pathOr( + [], + ["ProjectsReducer", "projects", projectUid, "tags"], + store + ); + }; -export const selectProfileStars = ( - profileUid: string -): ((store: IStore) => any) => (store) => { - return pathOr( - [], - ["ProfileReducer", "profiles", profileUid, "stars"], - store - ); -}; +export const selectProfileStars = + (profileUid: string) => (store: RootState) => { + return pathOr( + [], + ["ProfileReducer", "profiles", profileUid, "stars"], + store + ); + }; -export const selectAllUserProjectUids = ( - profileUid: string | undefined -): ((store: IStore) => any) => (store) => { - if (!profileUid) { - return []; - } else { - const allUserProjects = values( - pathOr({}, ["ProjectsReducer", "projects"], store) - ).filter((p) => p.userUid === profileUid); - return allUserProjects.map((p) => p.projectUid); - } -}; +export const selectAllUserProjectUids = + (profileUid: string | undefined) => (store: RootState) => { + if (profileUid) { + const allUserProjects: IProject[] = ( + Object.values( + store?.ProjectsReducer?.projects || + ({} as Record) + ) as IProject[] + ).filter((p: IProject) => p.userUid === profileUid); -export const selectAllTagsFromUser = ( - profileUid: string -): ((store: IStore) => any) => (store) => { - return pathOr( - [], - ["ProfileReducer", "profiles", profileUid, "allTags"], - store - ); -}; + return allUserProjects.map((p) => p.projectUid); + } else { + return []; + } + }; -export const selectProfileProjectsCount = ( - profileUid: string -): ((store: IStore) => any) => (store) => { - return pathOr( - { all: 0, default: 0 }, - ["ProfileReducer", "profiles", profileUid, "projectsCount"], - store - ); -}; +export const selectAllTagsFromUser = + (profileUid: string): ((store: RootState) => any) => + (store) => { + return pathOr( + [], + ["ProfileReducer", "profiles", profileUid, "allTags"], + store + ); + }; + +// Selector to get profileUid (customize this as per your state structure) +export const selectProfileUid = createSelector( + [(state: RootState) => state.LoginReducer.loggedInUid], // Replace with the correct state slice + (loggedInUid) => loggedInUid +); + +// Selector to get the projectsCount for the logged-in user +export const selectProfileProjectsCount = createSelector( + [selectProfiles, selectProfileUid], + (profiles, profileUid) => { + if (!profileUid || !profiles[profileUid]) { + return { + all: 0, + public: 0 + }; + } + return ( + profiles[profileUid].projectsCount ?? { + all: 0, + public: 0 + } + ); + } +); + +// Selector to get the projectsCount for a specific profileUid +export const selectUserProjectsCount = + (profileUid: string | undefined) => (store: RootState) => { + if (!profileUid) { + return { + all: 0, + public: 0 + }; + } + const profiles = selectProfiles(store); + if (!profiles[profileUid]) { + return { + all: 0, + public: 0 + }; + } + return ( + profiles[profileUid].projectsCount ?? { + all: 0, + public: 0 + } + ); + }; // export const selectProjectIconStyle = ( // projectUid: string @@ -236,3 +274,31 @@ export const selectProfileProjectsCount = ( // iconName: prop("iconName", proj) // }; // }; + +// Loading state selectors +export const selectFollowingLoading = + (profileUid: string) => (store: RootState) => { + return pathOr( + false, + ["ProfileReducer", "followingLoading", profileUid], + store + ); + }; + +export const selectFollowersLoading = + (profileUid: string) => (store: RootState) => { + return pathOr( + false, + ["ProfileReducer", "followersLoading", profileUid], + store + ); + }; + +export const selectStarsLoading = + (profileUid: string) => (store: RootState) => { + return pathOr( + false, + ["ProfileReducer", "starsLoading", profileUid], + store + ); + }; diff --git a/src/components/profile/styles.ts b/src/components/profile/styles.ts index e0eb9b39..59953886 100644 --- a/src/components/profile/styles.ts +++ b/src/components/profile/styles.ts @@ -1,6 +1,5 @@ import React from "react"; -import { Avatar, Theme as MaterialTheme } from "@material-ui/core"; -import { createStyles, withStyles } from "@material-ui/styles"; +import Avatar from "@mui/material/Avatar"; import styled from "@emotion/styled"; import { css, SerializedStyles, Theme } from "@emotion/react"; import { shadow } from "@styles/_common"; @@ -10,37 +9,6 @@ export const createButtonAddIcon = css` margin-left: 2px; `; -const profileStyles = (theme: MaterialTheme) => - createStyles({ - centerBox: { - position: "absolute", - width: "600px", - height: "50px", - top: "120px", - left: "50%", - marginTop: "-25px", - marginLeft: "-50px" - }, - startCodingButton: { - fontSize: "22px", - border: "4px solid #518C82", - borderRadius: "80%", - display: "flex", - alignItems: "center", - justifyContent: "center", - width: "220px", - height: "220px", - textDecoration: "none", - background: "#00DFCB" - } - }); - -// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types -const withStyles_ = (ClassComponent: any) => - withStyles(profileStyles)(ClassComponent); - -export default withStyles_; - export const iconPreviewBox = css` margin-left: -16px; margin-top: -16px; @@ -52,14 +20,6 @@ export const iconPreviewBox = css` cursor: pointer; `; -// export const previewAvatarColor = ( -// foregroundColor: string -// ): SerializedStyles => css` -// & > svg { -// ${avatarIconForeground(foregroundColor)} -// } -// `; - export const loadingSpinner = (theme: Theme): SerializedStyles => css` @keyframes cricle { from { diff --git a/src/components/profile/subscribers.tsx b/src/components/profile/subscribers.tsx index 6558e23f..9b8f6782 100644 --- a/src/components/profile/subscribers.tsx +++ b/src/components/profile/subscribers.tsx @@ -1,4 +1,4 @@ -import { store } from "@store/index"; +import { AppThunkDispatch, RootState, store } from "@root/store"; import { doc, getDoc, @@ -8,6 +8,7 @@ import { where } from "firebase/firestore"; import { + database, following, followers, profiles, @@ -25,7 +26,10 @@ import { convertProjectSnapToProject } from "@comp/projects/utils"; import { storeUserProfile, storeProfileProjectsCount, - storeProfileStars + storeProfileStars, + setFollowingLoading, + setFollowersLoading, + setStarsLoading } from "./actions"; import { IProfile, @@ -52,12 +56,16 @@ import { IProject } from "../projects/types"; export const subscribeToProfile = ( profileUid: string, - dispatch: (any) => void + dispatch: AppThunkDispatch ): (() => void) => { const unsubscribe: () => void = onSnapshot( doc(profiles, profileUid), (profile) => { - dispatch(storeUserProfile(profile.data() as IProfile, profileUid)); + const profileData = profile.data() as any; + if (profileData.userJoinDate) { + profileData.userJoinDate = profileData.userJoinDate.toMillis(); + } + dispatch(storeUserProfile(profileData as IProfile, profileUid)); }, (error: any) => console.error(error) ); @@ -66,8 +74,11 @@ export const subscribeToProfile = ( export const subscribeToFollowing = ( profileUid: string, - dispatch: (any) => void + dispatch: AppThunkDispatch ): (() => void) => { + // Set loading state + dispatch(setFollowingLoading(profileUid, true)); + const unsubscribe: () => void = onSnapshot( doc(following, profileUid), async (followingReference) => { @@ -82,16 +93,21 @@ export const subscribeToFollowing = ( const missingProfileUids = difference( userProfileUids, cachedProfileUids - ); + ) as string[]; const missingProfiles = await Promise.all( - missingProfileUids.map(async (followingProfileUid) => { + missingProfileUids.map(async (followingProfileUid: string) => { const profPromise = await getDoc( doc(profiles, followingProfileUid) ); - return profPromise.exists() + const profileData = profPromise.exists() ? profPromise.data() : { displayName: "Deleted user" }; + if (profileData.userJoinDate) { + profileData.userJoinDate = + profileData.userJoinDate.toMillis(); + } + return profileData; }) ); dispatch({ @@ -100,16 +116,25 @@ export const subscribeToFollowing = ( userProfiles: missingProfiles, userProfileUids }); + // Clear loading state + dispatch(setFollowingLoading(profileUid, false)); }, - (error: any) => console.error(error) + (error: any) => { + console.error(error); + // Clear loading state on error + dispatch(setFollowingLoading(profileUid, false)); + } ); return unsubscribe; }; export const subscribeToFollowers = ( profileUid: string, - dispatch: (any) => void + dispatch: AppThunkDispatch ): (() => void) => { + // Set loading state + dispatch(setFollowersLoading(profileUid, true)); + const unsubscribe: () => void = onSnapshot( doc(followers, profileUid), async (followersReference) => { @@ -124,17 +149,23 @@ export const subscribeToFollowers = ( const missingProfileUids = difference( userProfileUids, cachedProfileUids - ); + ) as string[]; const missingProfiles = await Promise.all( - missingProfileUids.map(async (followerProfileUid) => { + missingProfileUids.map(async (followerProfileUid: string) => { const profPromise = await getDoc( doc(profiles, followerProfileUid) ); - return profPromise.exists() + const profileData = profPromise.exists() ? profPromise.data() : { displayName: "Deleted user" }; + + if (profileData.userJoinDate) { + profileData.userJoinDate = + profileData.userJoinDate.toMillis(); + } + return profileData; }) ); dispatch({ @@ -143,15 +174,21 @@ export const subscribeToFollowers = ( userProfiles: missingProfiles, userProfileUids }); + // Clear loading state + dispatch(setFollowersLoading(profileUid, false)); }, - (error: any) => console.error(error) + (error: any) => { + console.error(error); + // Clear loading state on error + dispatch(setFollowersLoading(profileUid, false)); + } ); return unsubscribe; }; export const subscribeToProjectsCount = ( profileUid: string, - dispatch: (any) => void + dispatch: AppThunkDispatch ): (() => void) => { const unsubscribe: () => void = onSnapshot( doc(projectsCount, profileUid), @@ -175,25 +212,60 @@ export const subscribeToProjectsCount = ( export const subscribeToProfileStars = ( profileUid: string, - dispatch: (any) => void + dispatch: AppThunkDispatch ): (() => void) => { + // Set loading state + dispatch(setStarsLoading(profileUid, true)); + const unsubscribe: () => void = onSnapshot( - doc(profileStars), + doc(profileStars, profileUid), (starsReference) => { const starsData = starsReference.data(); if (!starsData) { + // Clear loading state when no data is found + dispatch(setStarsLoading(profileUid, false)); return; } + + // Convert Firestore Timestamps to serializable values + const serializedStarsData: Record = {}; + for (const projectUid in starsData) { + const timestamp = starsData[projectUid]; + if (timestamp && typeof timestamp.toDate === "function") { + // Convert Firestore Timestamp to milliseconds + serializedStarsData[projectUid] = { + toDate: timestamp.toDate().getTime() + }; + } else { + // Handle case where it's already serialized or different format + serializedStarsData[projectUid] = timestamp; + } + } + const state = store.getState(); - const starredProjects = keys(starsData); - const cachedProjects = keys(state.ProjectsReducer.projects); + const starredProjects = Object.keys(starsData); + const cachedProjects = Object.keys(state.ProjectsReducer.projects); const missingProjects = difference(starredProjects, cachedProjects); - missingProjects.forEach((projectUid) => - dispatch(downloadProjectOnce(projectUid)) - ); - dispatch(storeProfileStars(starsData, profileUid)); + missingProjects.forEach(async (projectUid) => { + try { + await dispatch(downloadProjectOnce(projectUid)); + } catch (error) { + console.error( + "Error downloading project:", + projectUid, + error + ); + } + }); + dispatch(storeProfileStars(serializedStarsData, profileUid)); + // Clear loading state + dispatch(setStarsLoading(profileUid, false)); }, - (error: any) => console.error(error) + (error: any) => { + console.error(error); + // Clear loading state on error + dispatch(setStarsLoading(profileUid, false)); + } ); return unsubscribe; }; @@ -201,17 +273,15 @@ export const subscribeToProfileStars = ( export const subscribeToProfileProjects = ( profileUid: string, isProfileOwner: boolean, - dispatch: (any) => void + dispatch: AppThunkDispatch ): (() => void) => { const unsubscribe = onSnapshot( isProfileOwner ? query(projects, where("userUid", "==", profileUid)) : query( projects, + where("userUid", "==", profileUid), where("public", "==", true) - // query( - // query(projects, where("userUid", "==", profileUid)), - // ) ), async (projectSnaps) => { const currentProfileProjects = pipe( @@ -220,7 +290,7 @@ export const subscribeToProfileProjects = ( )(store.getState()); const projectsDeleted = difference( keys(currentProfileProjects).sort(), - map(prop("id"), projectSnaps.docs).sort() + (projectSnaps.docs as any[]).map((snapDoc) => snapDoc.id).sort() ); if (!projectSnaps.empty) { Promise.all( @@ -228,9 +298,8 @@ export const subscribeToProfileProjects = ( const projTags = await getDocs( query(tags, where(projSnap.id, "==", profileUid)) ).then((d) => d.docs.map(prop("id"))); - const proj = await convertProjectSnapToProject( - projSnap - ); + const proj = + await convertProjectSnapToProject(projSnap); return assoc("tags", projTags, proj) as IProject; }) ).then((localProjects) => { diff --git a/src/components/profile/tabs/followers-list.tsx b/src/components/profile/tabs/followers-list.tsx index 874a6462..ee9a334b 100644 --- a/src/components/profile/tabs/followers-list.tsx +++ b/src/components/profile/tabs/followers-list.tsx @@ -4,26 +4,79 @@ import { StyledListItemTopRowText, StyledUserListItemContainer } from "../profile-ui"; -import { ListItem, Avatar, ListItemText } from "@material-ui/core"; -import { useDispatch } from "react-redux"; -import { push } from "connected-react-router"; +import { + Avatar, + ListItemText, + ListItemButton, + Typography, + Box, + CircularProgress +} from "@mui/material"; +import { useNavigate } from "react-router"; +import PersonIcon from "@mui/icons-material/Person"; -const FollowersList = ({ - filteredFollowers +export const FollowersList = ({ + filteredFollowers, + isLoading = false }: { filteredFollowers: Array; + isLoading?: boolean; }): React.ReactElement => { - const dispatch = useDispatch(); + const navigate = useNavigate(); + + if (isLoading) { + return ( + + + + ); + } + + if (!filteredFollowers || filteredFollowers.length === 0) { + return ( + + + + No Followers Yet + + + This user doesn't have any followers yet. + + + ); + } + return ( <> {filteredFollowers.map((p: any, index) => { return ( - { - dispatch(push(`/profile/${p.username}`)); + navigate(`/profile/${p.username}`); }} > @@ -38,7 +91,7 @@ const FollowersList = ({ /> - + ); })} diff --git a/src/components/profile/tabs/following-list.tsx b/src/components/profile/tabs/following-list.tsx index b4dc82c8..cd350d02 100644 --- a/src/components/profile/tabs/following-list.tsx +++ b/src/components/profile/tabs/following-list.tsx @@ -4,26 +4,79 @@ import { StyledListItemTopRowText, StyledUserListItemContainer } from "../profile-ui"; -import { ListItem, Avatar, ListItemText } from "@material-ui/core"; -import { useDispatch } from "react-redux"; -import { push } from "connected-react-router"; +import { + ListItemButton, + Avatar, + ListItemText, + Typography, + Box, + CircularProgress +} from "@mui/material"; +import { useNavigate } from "react-router"; +import PeopleIcon from "@mui/icons-material/People"; -const FollowingList = ({ - filteredFollowing +export const FollowingList = ({ + filteredFollowing, + isLoading = false }: { filteredFollowing: Array; -}): React.ReactElement => { - const dispatch = useDispatch(); + isLoading?: boolean; +}) => { + const navigate = useNavigate(); + + if (isLoading) { + return ( + + + + ); + } + + if (!filteredFollowing || filteredFollowing.length === 0) { + return ( + + + + No Following Yet + + + This user isn't following anyone yet. + + + ); + } + return ( <> {filteredFollowing.map((p: any, index) => { return ( - { - dispatch(push(`/profile/${p.username}`)); + navigate(`/profile/${p.username}`); }} > @@ -38,11 +91,9 @@ const FollowingList = ({ /> - + ); })} ); }; - -export default FollowingList; diff --git a/src/components/profile/tabs/stars-list.tsx b/src/components/profile/tabs/stars-list.tsx index f5611913..6e9f81a5 100644 --- a/src/components/profile/tabs/stars-list.tsx +++ b/src/components/profile/tabs/stars-list.tsx @@ -1,5 +1,4 @@ -import React from "react"; -import { IStore } from "@store/types"; +import { RootState } from "@root/store"; import { StyledListItemTopRowText, StyledUserListItemContainer @@ -7,52 +6,116 @@ import { import ProjectAvatar from "@elem/project-avatar"; import { IProject } from "@comp/projects/types"; import { selectProfileStars } from "../selectors"; -import { ListItem, ListItemText } from "@material-ui/core"; -import { useDispatch, useSelector } from "react-redux"; -import { push } from "connected-react-router"; -import { isEmpty, prop } from "ramda"; +import { + ListItemButton, + ListItemText, + Typography, + Box, + CircularProgress +} from "@mui/material"; +import { useSelector } from "react-redux"; +import { useNavigate } from "react-router"; +import { isEmpty } from "ramda"; +import StarIcon from "@mui/icons-material/Star"; import * as SS from "./styles"; -const StarsList = ({ - profileUid +export const StarsList = ({ + profileUid, + isLoading = false }: { profileUid: string; -}): React.ReactElement => { - const dispatch = useDispatch(); + isLoading?: boolean; +}) => { + const navigate = useNavigate(); const profileStars = useSelector(selectProfileStars(profileUid)); const cachedProjects = useSelector( - (store: IStore) => store.ProjectsReducer.projects + (store: RootState) => store.ProjectsReducer.projects ); - return isEmpty(profileStars) ? ( - <> - ) : ( - profileStars.map((projectUid, index) => { - const project: IProject = prop(projectUid, cachedProjects); - return ( - { - dispatch(push(`/editor/${projectUid}`)); + + if (isLoading) { + return ( + + + + ); + } + + if (isEmpty(profileStars)) { + return ( + + + + No Starred Projects + + - -
- {project && } -
- - - -
-
- ); - }) + This user hasn't starred any projects yet. +
+ + ); + } + + return ( + <> + {profileStars.map((projectUid, index) => { + const project: IProject = cachedProjects[projectUid]!; + + return ( + { + navigate(`/editor/${projectUid}`); + }} + > + +
+ {project && ( + + )} +
+ + + +
+
+ ); + })} + ); }; - -export default StarsList; diff --git a/src/components/profile/tag-auto-suggest.jsx b/src/components/profile/tag-auto-suggest.jsx deleted file mode 100644 index 298adaf6..00000000 --- a/src/components/profile/tag-auto-suggest.jsx +++ /dev/null @@ -1,156 +0,0 @@ -// eslint-disable-next-line no-use-before-define -import React, { useState } from "react"; -import { useSelector, useDispatch } from "react-redux"; -import Autosuggest from "react-autosuggest"; -import match from "autosuggest-highlight/match"; -import parse from "autosuggest-highlight/parse"; -import Paper from "@material-ui/core/Paper"; -import MenuItem from "@material-ui/core/MenuItem"; -import { withStyles } from "@material-ui/core/styles"; -import ChipInput from "material-ui-chip-input"; -import { selectLoggedInUid } from "@comp/login/selectors"; -import { selectAllTagsFromUser, selectCurrentTagText } from "./selectors"; -import { setCurrentTagText } from "./actions"; -import { append, dissoc, equals, pickAll, pipe, reject, values } from "ramda"; -import Fuse from "fuse.js"; - -const styles = (theme) => ({ - container: { - flexGrow: 1, - position: "relative" - }, - suggestionsContainerOpen: { - position: "absolute", - marginTop: theme.spacing(), - marginBottom: theme.spacing(3), - left: 0, - right: 0, - zIndex: 1 - }, - suggestion: { - display: "block" - }, - suggestionsList: { - margin: 0, - padding: 0, - listStyleType: "none" - }, - textField: { - width: "100%" - } -}); - -const TagAutosuggest = (properties) => { - const { - allowDuplicates, - classes, - modifiedTags, - setModifiedTags, - ...other - } = properties; - const dispatch = useDispatch(); - const loggedInUserUid = useSelector(selectLoggedInUid); - const allTags = useSelector(selectAllTagsFromUser(loggedInUserUid)); - const [suggestions, setSuggestions] = useState([]); - const textFieldInput = useSelector(selectCurrentTagText); - const handleSuggestionsFetchRequested = ({ value }) => { - const options = { - shouldSort: true - }; - const fuse = new Fuse(allTags, options); - const matches = fuse.search(value); - const suggest = values(pickAll(matches, allTags)); - setSuggestions(suggest); - }; - - const handleSuggestionsClearRequested = () => { - dispatch(setCurrentTagText("")); - }; - - const handletextFieldInputChange = (event, { newValue }) => { - dispatch(setCurrentTagText(newValue)); - }; - - const handleAddChip = (chip) => { - if (allowDuplicates || !allTags.includes(chip)) { - setModifiedTags(append(chip, modifiedTags)); - dispatch(setCurrentTagText("")); - } - }; - - const handleDeleteChip = (chip) => { - setModifiedTags(reject(equals(chip), modifiedTags)); - }; - - return ( - s} - onSuggestionSelected={(event, { suggestionValue }) => { - handleAddChip(suggestionValue); - event.preventDefault(); - }} - focusInputOnSuggestionClick={false} - inputProps={{ - chips: modifiedTags, - value: textFieldInput, - onChange: handletextFieldInputChange, - onAdd: (chip) => handleAddChip(chip), - onDelete: (chip, index) => handleDeleteChip(chip, index), - ...other - }} - renderInputComponent={({ onChange, chips, ref, ...other }) => ( - - )} - suggestions={suggestions} - onSuggestionsFetchRequested={handleSuggestionsFetchRequested} - onSuggestionsClearRequested={handleSuggestionsClearRequested} - renderSuggestionsContainer={({ containerProps, children }) => ( - - {children} - - )} - renderSuggestion={(suggestion, { query, isHighlighted }) => { - const matches = match(suggestion, query); - const parts = parse(suggestion, matches); - - return ( - event.preventDefault()} // prevent the click causing the input to be blurred - > -
- {parts.map((part, index) => { - return part.highlight ? ( - - {part.text} - - ) : ( - {part.text} - ); - })} -
-
- ); - }} - /> - ); -}; - -export default withStyles(styles)(TagAutosuggest); diff --git a/src/components/profile/tag-auto-suggest.tsx b/src/components/profile/tag-auto-suggest.tsx new file mode 100644 index 00000000..1eb4317f --- /dev/null +++ b/src/components/profile/tag-auto-suggest.tsx @@ -0,0 +1,218 @@ +/* eslint-disable */ +import React, { useState, useCallback } from "react"; +import { useSelector, useDispatch } from "react-redux"; +// @ts-expect-error No type definitions available for react-autosuggest +import Autosuggest from "react-autosuggest"; +// @ts-expect-error No type definitions available for autosuggest-highlight +import match from "autosuggest-highlight/match"; +// @ts-expect-error No type definitions available for autosuggest-highlight +import parse from "autosuggest-highlight/parse"; +import Paper from "@mui/material/Paper"; +import MenuItem from "@mui/material/MenuItem"; +import ChipInput from "./chip-input"; +import { selectLoggedInUid } from "@comp/login/selectors"; +import { selectAllTagsFromUser, selectCurrentTagText } from "./selectors"; +import { setCurrentTagText } from "./actions"; +import { append, dissoc, equals, pickAll, pipe, reject, values } from "ramda"; +import Fuse from "fuse.js"; + +interface TagAutosuggestProps { + allowDuplicates?: boolean; + modifiedTags: string[]; + setModifiedTags: (tags: string[]) => void; + [key: string]: any; +} + +interface SuggestionsFetchRequestedParams { + value: string; +} + +interface SuggestionSelectedParams { + suggestionValue: string; +} + +interface RenderSuggestionParams { + query: string; + isHighlighted: boolean; +} + +interface RenderInputComponentProps { + onChange: (event: React.ChangeEvent) => void; + chips: string[]; + ref: React.Ref | null; + [key: string]: any; +} + +interface HighlightPart { + text: string; + highlight: boolean; +} + +interface RenderSuggestionsContainerProps { + containerProps: any; + children: React.ReactNode; +} + +const TagAutosuggest: React.FC = ({ + allowDuplicates, + modifiedTags, + setModifiedTags, + ...other +}) => { + const dispatch = useDispatch(); + const loggedInUserUid = useSelector(selectLoggedInUid); + const allTags = useSelector(selectAllTagsFromUser(loggedInUserUid || "")); + const [suggestions, setSuggestions] = useState([]); + const textFieldInput = useSelector(selectCurrentTagText); + + const handleSuggestionsFetchRequested = useCallback( + ({ value }: SuggestionsFetchRequestedParams) => { + const options = { + shouldSort: true + }; + const fuse = new Fuse(allTags, options); + const matches = fuse.search(value); + const matchIndices = matches.map((match: any) => match.refIndex); + const suggest = values(pickAll(matchIndices, allTags)); + setSuggestions(suggest); + }, + [allTags] + ); + + const handleSuggestionsClearRequested = useCallback(() => { + dispatch(setCurrentTagText("")); + }, [dispatch]); + + const handleTextFieldInputChange = useCallback( + (event: any, { newValue }: { newValue: string }) => { + dispatch(setCurrentTagText(newValue)); + }, + [dispatch] + ); + + const handleAddChip = useCallback( + (chip: string) => { + if (allowDuplicates || !allTags.includes(chip)) { + setModifiedTags(append(chip, modifiedTags)); + dispatch(setCurrentTagText("")); + } + }, + [allowDuplicates, allTags, modifiedTags, setModifiedTags, dispatch] + ); + + const handleDeleteChip = useCallback( + (chip: string) => { + setModifiedTags(reject(equals(chip), modifiedTags)); + }, + [modifiedTags, setModifiedTags] + ); + + const getSuggestionValue = useCallback( + (suggestion: string) => suggestion, + [] + ); + + const onSuggestionSelected = useCallback( + ( + event: React.FormEvent, + { suggestionValue }: SuggestionSelectedParams + ) => { + handleAddChip(suggestionValue); + event.preventDefault(); + }, + [handleAddChip] + ); + + const renderInputComponent = useCallback( + ({ + onChange, + chips, + ref, + key, + ...otherProps + }: RenderInputComponentProps & { key?: React.Key }) => ( + void) | undefined + } + {...pipe(dissoc("projectUid"), dissoc("value"))(otherProps)} + /> + ), + [] + ); + + const renderSuggestionsContainer = useCallback( + ({ containerProps, children }: RenderSuggestionsContainerProps) => { + const { key, ...otherContainerProps } = containerProps; + return ( + + {children} + + ); + }, + [] + ); + + const renderSuggestion = useCallback( + ( + suggestion: string, + { query, isHighlighted }: RenderSuggestionParams + ) => { + const matches = match(suggestion, query); + const parts = parse(suggestion, matches); + + return ( + event.preventDefault()} + > +
+ {parts.map((part: HighlightPart, index: number) => { + return part.highlight ? ( + + {part.text} + + ) : ( + {part.text} + ); + })} +
+
+ ); + }, + [] + ); + + return ( + + ); +}; + +export default TagAutosuggest; diff --git a/src/components/profile/types.ts b/src/components/profile/types.ts index 698fa82b..c22ad564 100644 --- a/src/components/profile/types.ts +++ b/src/components/profile/types.ts @@ -5,7 +5,6 @@ export const STORE_PROFILE_PROJECTS_COUNT = "PROFILE.STORE_PROFILE_PROJECTS_COUNT"; export const STORE_PROFILE_STARS = "PROFILE.STORE_PROFILE_STARS"; export const ADD_USER_PROJECT = "PROFILE.ADD_USER_PROJECT"; -export const DELETE_USER_PROJECT = "PROFILE.DELETE_USER_PROJECT"; export const SET_CURRENT_TAG_TEXT = "PROFILE.SET_CURRENT_TAG_TEXT"; export const SET_TAGS_INPUT = "PROFILE.SET_TAGS_INPUT"; export const SET_USER_PROFILE = "PROFILE.SET_USER_PROFILE"; @@ -24,10 +23,12 @@ export const UPDATE_PROFILE_FOLLOWING = "PROFILE.UPDATE_PROFILE_FOLLOWING"; export const UPDATE_PROFILE_FOLLOWERS = "PROFILE.UPDATE_PROFILE_FOLLOWERS"; export const GET_USER_PROFILES_FOR_FOLLOWING = "PROFILE.GET_USER_PROFILES_FOR_FOLLOWING"; +export const SET_FOLLOWING_LOADING = "PROFILE.SET_FOLLOWING_LOADING"; +export const SET_FOLLOWERS_LOADING = "PROFILE.SET_FOLLOWERS_LOADING"; +export const SET_STARS_LOADING = "PROFILE.SET_STARS_LOADING"; type ProfileActionTypeValue = | typeof SET_CURRENT_TAG_TEXT - | typeof DELETE_USER_PROJECT | typeof ADD_USER_PROJECT | typeof STORE_USER_PROFILE | typeof GET_ALL_TAGS @@ -42,7 +43,10 @@ type ProfileActionTypeValue = | typeof UPDATE_LOGGED_IN_FOLLOWING | typeof UPDATE_PROFILE_FOLLOWING | typeof SET_PROJECT_FILTER_STRING - | typeof SET_FOLLOWING_FILTER_STRING; + | typeof SET_FOLLOWING_FILTER_STRING + | typeof SET_FOLLOWING_LOADING + | typeof SET_FOLLOWERS_LOADING + | typeof SET_STARS_LOADING; export type ProjectsCount = { all: number; @@ -57,12 +61,17 @@ export type ProfileActionTypes = { export interface IProfile { allTags?: any[]; bio?: string; - profileUid?: string; + userUid: string; userFollowing?: []; + followers?: []; projectsCount?: ProjectsCount; userImageURL?: string; backgroundIndex: number; displayName: string; + link1: string; + link2: string; + link3: string; username: string; photoUrl?: string; + userJoinDate?: number; } diff --git a/src/components/project-editor/actions.tsx b/src/components/project-editor/actions.tsx index 6b7e7fe0..f1397586 100644 --- a/src/components/project-editor/actions.tsx +++ b/src/components/project-editor/actions.tsx @@ -1,14 +1,10 @@ -import React from "react"; -import Button from "@material-ui/core/Button"; -import { EditorView } from "codemirror"; -import { closeModal, openSimpleModal } from "@comp/modal/actions"; -import { resetDocumentValue } from "@comp/projects/actions"; +import { openSimpleModal } from "@comp/modal/actions"; import { IDocument } from "@comp/projects/types"; import { ITarget } from "@comp/target-controls/types"; -import { find, propEq } from "ramda"; import { sortByStoredTabOrder } from "./utils"; import { MANUAL_LOOKUP_STRING, + TAB_DOCK_OPEN_NON_CLOUD_FILE, TAB_DOCK_OPEN_TAB_BY_DOCUMENT_UID, TAB_DOCK_CLOSE, TAB_DOCK_INIT, @@ -18,7 +14,6 @@ import { TOGGLE_MANUAL_PANEL, SET_MANUAL_PANEL_OPEN, SET_FILE_TREE_PANEL_OPEN, - STORE_EDITOR_INSTANCE, IOpenDocument } from "./types"; @@ -26,7 +21,7 @@ export const tabDockInit = ( projectUid: string, allDocuments: IDocument[], defaultTarget: ITarget | undefined -): ((dispatch: any) => Promise) => { +) => { const storedIndex = localStorage.getItem(projectUid + ":tabIndex"); const storedTabOrder: string | null = localStorage.getItem( projectUid + ":tabOrder" @@ -62,6 +57,7 @@ export const tabDockInit = ( console.error(error); } } + if ( defaultTarget && defaultTarget.targetDocumentUid && @@ -69,8 +65,7 @@ export const tabDockInit = ( allDocuments.length > 0 ) { initialOpenDocuments.push({ - uid: defaultTarget.targetDocumentUid, - editorInstance: undefined + uid: defaultTarget.targetDocumentUid }); } else if ( defaultTarget && @@ -80,31 +75,54 @@ export const tabDockInit = ( ) { defaultTarget.playlistDocumentsUid.forEach((documentUid) => { initialOpenDocuments.push({ - uid: documentUid, - editorInstance: undefined + uid: documentUid }); }); } else if ( - /* eslint-disable-next-line unicorn/no-useless-length-check */ allDocuments.length > 0 && - /* eslint-disable-next-line unicorn/no-useless-length-check */ allDocuments.some((d) => d.filename === "project.csd") ) { - const projectCsd = find( - propEq("filename", "project.csd"), - allDocuments + const projectCsd = allDocuments.find( + (d) => d.filename === "project.csd" ); + projectCsd && - !find( - propEq("uid", projectCsd.documentUid), - initialOpenDocuments + !initialOpenDocuments.find( + (d) => d.uid === projectCsd.documentUid ) && initialOpenDocuments.push({ - uid: projectCsd.documentUid, - editorInstance: undefined + uid: projectCsd.documentUid }); } + // Additional fallback logic for when no tabs are open yet + if (initialOpenDocuments.length === 0 && allDocuments.length > 0) { + // First, try to find any .csd file + const csdFiles = allDocuments.filter((d) => + d.filename.endsWith(".csd") + ); + if (csdFiles.length > 0) { + initialOpenDocuments.push({ + uid: csdFiles[0].documentUid + }); + } else { + // If no .csd files, try to find any .orc file + const orcFiles = allDocuments.filter((d) => + d.filename.endsWith(".orc") + ); + if (orcFiles.length > 0) { + initialOpenDocuments.push({ + uid: orcFiles[0].documentUid + }); + } else if (allDocuments.length === 1) { + // If there's only 1 file/document, open it + initialOpenDocuments.push({ + uid: allDocuments[0].documentUid + }); + } + } + } + if (initialOpenDocuments.length > 0 && initialIndex < 0) { initialIndex = 0; } @@ -143,130 +161,75 @@ export const tabOpenByDocumentUid = ( }; export const tabOpenNonCloudDocument = ( - documentName: string -): ((dispatch: any) => Promise) => { - return async (dispatch: any) => { - dispatch({ - type: TAB_DOCK_OPEN_TAB_BY_DOCUMENT_UID, - isNonCloudDocument: true, - documentUid: documentName - }); + filename: string, + mimeType: string +): { + type: typeof TAB_DOCK_OPEN_NON_CLOUD_FILE; + isNonCloudDocument: boolean; + filename: string; + mimeType: string; +} => { + return { + type: TAB_DOCK_OPEN_NON_CLOUD_FILE, + isNonCloudDocument: true, + filename, + mimeType }; }; -export const closeTabDock = (): Record => { +export const closeTabDock = () => { return { type: TAB_DOCK_CLOSE }; }; -const closeUnsavedTabPrompt = ( - cancelCallback, - closeWithoutSavingCallback -): (() => React.ReactElement) => { - return function CloseUnsavedFilePrompt() { - return ( -
- - -
- ); - }; -}; - export const tabClose = ( activeProjectUid: string, documentUid: string, isModified: boolean -): ((dispatch: any) => Promise) => { - return async (dispatch: any) => { - if (!isModified) { - dispatch({ - type: TAB_CLOSE, - projectUid: activeProjectUid, - documentUid - }); - } else { - const cancelCallback = () => dispatch(closeModal()); - const closeWithoutSavingCallback = () => { - dispatch(closeModal()); - dispatch({ - type: TAB_CLOSE, - documentUid - }); - dispatch(resetDocumentValue(activeProjectUid, documentUid)); - }; - - const closeUnsavedTabPromptComp = closeUnsavedTabPrompt( - cancelCallback, - closeWithoutSavingCallback - ); - dispatch(openSimpleModal(closeUnsavedTabPromptComp, {})); - } - }; +) => { + return isModified + ? openSimpleModal("close-unsaved-prompt", { + projectUid: activeProjectUid, + documentUid + }) + : { + type: TAB_CLOSE, + projectUid: activeProjectUid, + documentUid + }; }; -export const tabSwitch = (index: number): Record => { +export const tabSwitch = (index: number) => { return { type: TAB_DOCK_SWITCH_TAB, tabIndex: index }; }; -export const lookupManualString = ( - manualLookupString?: string -): Record => { +export const lookupManualString = (manualLookupString?: string) => { return { type: MANUAL_LOOKUP_STRING, manualLookupString }; }; -export const toggleManualPanel = (): Record => { +export const toggleManualPanel = () => { return { type: TOGGLE_MANUAL_PANEL }; }; -export const setManualPanelOpen = (open: boolean): Record => { +export const setManualPanelOpen = (open: boolean) => { return { type: SET_MANUAL_PANEL_OPEN, open }; }; -export const setFileTreePanelOpen = (open: boolean): Record => { +export const setFileTreePanelOpen = (open: boolean) => { return { type: SET_FILE_TREE_PANEL_OPEN, open }; }; - -export const storeEditorInstance = ( - editorInstance: EditorView | undefined, - projectUid: string, - documentUid: string -): ((dispatch: any) => Promise) => { - return async (dispatch: any) => { - dispatch({ - type: STORE_EDITOR_INSTANCE, - editorInstance, - projectUid, - documentUid - }); - }; -}; diff --git a/src/components/project-editor/csound-manual.tsx b/src/components/project-editor/csound-manual.tsx index e8e1d94e..8cd31bd9 100644 --- a/src/components/project-editor/csound-manual.tsx +++ b/src/components/project-editor/csound-manual.tsx @@ -1,11 +1,10 @@ import React, { useEffect } from "react"; import { useDispatch, useSelector } from "react-redux"; import { useTheme } from "@emotion/react"; -import { IStore } from "@store/types"; +import { RootState } from "@root/store"; +import DisabledByDefaultRoundedIcon from "@mui/icons-material/DisabledByDefaultRounded"; import { windowHeader as windowHeaderStyle } from "@styles/_common"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faWindowClose } from "@fortawesome/free-solid-svg-icons"; -import Tooltip from "@material-ui/core/Tooltip"; +import Tooltip from "@mui/material/Tooltip"; import IframeComm from "react-iframe-comm"; import { setManualPanelOpen } from "./actions"; import * as SS from "./styles"; @@ -21,7 +20,7 @@ const ManualWindow = ({ const theme: any = useTheme(); const manualLookupString = useSelector( - (store: IStore) => store.ProjectEditorReducer.manualLookupString + (store: RootState) => store.ProjectEditorReducer.manualLookupString ); // const onManualMessage = (event_) => { @@ -61,10 +60,10 @@ const ManualWindow = ({ dispatch(setManualPanelOpen(false)) } > - diff --git a/src/components/project-editor/mobile-navigation.tsx b/src/components/project-editor/mobile-navigation.tsx index 3e14cdc2..de8de1f3 100644 --- a/src/components/project-editor/mobile-navigation.tsx +++ b/src/components/project-editor/mobile-navigation.tsx @@ -1,9 +1,10 @@ import React from "react"; -import { AccountTree, FormatTextdirectionLToR } from "@material-ui/icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faStream, faBook } from "@fortawesome/free-solid-svg-icons"; -import BottomNavigation from "@material-ui/core/BottomNavigation"; -import BottomNavigationAction from "@material-ui/core/BottomNavigationAction"; +import AutoStoriesRoundedIcon from "@mui/icons-material/AutoStoriesRounded"; +import ListAltRoundedIcon from "@mui/icons-material/ListAltRounded"; +import AccountTree from "@mui/icons-material/AccountTree"; +import FormatTextdirectionLToR from "@mui/icons-material/FormatTextdirectionLToR"; +import BottomNavigation from "@mui/material/BottomNavigation"; +import BottomNavigationAction from "@mui/material/BottomNavigationAction"; import * as SS from "./styles"; const MobileNavigation = ({ @@ -11,7 +12,7 @@ const MobileNavigation = ({ setMobileTabIndex }: { mobileTabIndex: number; - setMobileTabIndex: (number) => void; + setMobileTabIndex: (index: number) => void; }): React.ReactElement => { return ( setMobileTabIndex(2)} css={SS.mobileNavigationButton} label="Console" - icon={ - - } + icon={} > setMobileTabIndex(3)} css={SS.mobileNavigationButton} label="Manual" - icon={ - - } + icon={} > ); diff --git a/src/components/project-editor/modals.tsx b/src/components/project-editor/modals.tsx new file mode 100644 index 00000000..8bf4acc9 --- /dev/null +++ b/src/components/project-editor/modals.tsx @@ -0,0 +1,49 @@ +import React, { useCallback } from "react"; +import Button from "@mui/material/Button"; +import { useDispatch } from "@root/store"; +import { closeModal } from "@comp/modal/actions"; +import { resetDocumentValue } from "@comp/projects/actions"; +import { TAB_CLOSE } from "./types"; + +export function CloseUnsavedFilePrompt({ + projectUid, + documentUid +}: { + projectUid: string; + documentUid: string; +}) { + const dispatch = useDispatch(); + const cancelCallback = useCallback( + () => dispatch(closeModal()), + [dispatch] + ); + const closeWithoutSavingCallback = useCallback(() => { + dispatch(closeModal()); + dispatch({ + type: TAB_CLOSE, + documentUid + }); + dispatch(resetDocumentValue(projectUid, documentUid)); + }, [dispatch, documentUid, projectUid]); + + return ( +
+ + +
+ ); +} diff --git a/src/components/project-editor/project-editor.tsx b/src/components/project-editor/project-editor.tsx index 5998adc9..16544812 100644 --- a/src/components/project-editor/project-editor.tsx +++ b/src/components/project-editor/project-editor.tsx @@ -1,17 +1,17 @@ import React, { useEffect, useState } from "react"; -import { useDispatch, useSelector } from "react-redux"; +import { RootState, useDispatch, useSelector } from "@root/store"; import { selectIsOwner } from "./selectors"; import { DnDProvider } from "@comp/file-tree/context"; import { NonCloudFile } from "@comp/file-tree/types"; -import { CsoundObj } from "@csound/browser"; import { IDocument, IProject } from "@comp/projects/types"; import { Tabs, DragTabList, DragTab, PanelList, - Panel -} from "@hlolli/react-tabtab"; + Panel, + TabListComponent +} from "@root/tabtab/index.js"; import { arrayMoveImmutable as simpleSwitch } from "array-move"; import { subscribeToProjectLastModified } from "@comp/project-last-modified/subscribers"; import { @@ -19,26 +19,19 @@ import { subscribeToProjectsCount } from "@comp/profile/subscribers"; import tabStyles, { tabListStyle } from "./tab-styles"; +import { isAudioFile } from "../projects/utils"; import { Beforeunload } from "react-beforeunload"; -import Tooltip from "@material-ui/core/Tooltip"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faTimes } from "@fortawesome/free-solid-svg-icons"; -import IconButton from "@material-ui/core/IconButton"; import { IOpenDocument } from "./types"; import SplitPane_ from "react-split-pane"; -import { IStore } from "@store/types"; import Editor from "../editor/editor"; -import AudioEditor from "../audio-editor/audio-editor"; -import { useTheme } from "@emotion/react"; +import { AudioEditor } from "../audio-editor/audio-editor"; import { subscribeToProjectChanges } from "@comp/projects/subscribers"; -// import { toggleEditorFullScreen } from "../Editor/actions"; import CsoundManualWindow from "./csound-manual"; -import FileTree from "../file-tree"; +import { FileTree } from "../file-tree"; import { storeEditorKeyboardCallbacks, storeProjectEditorKeyboardCallbacks } from "@comp/hot-keys/actions"; -import { append, reduce, pathOr, propEq, propOr } from "ramda"; import { find, isEmpty } from "lodash"; import { rearrangeTabs, @@ -46,11 +39,8 @@ import { tabSwitch, setManualPanelOpen } from "./actions"; -import { mapIndexed, isMobile } from "@root/utils"; -import { isAudioFile, isPlotDataFile } from "../projects/utils"; -import DataPlotter from "@comp/data-plotter/data-plotter"; +import { isMobile } from "@root/utils"; import * as SS from "./styles"; -import { enableMidiInput, enableAudioInput } from "../csound/actions"; import BottomTabs from "@comp/bottom-tabs/component"; import MobileTabs from "@comp/bottom-tabs/mobile-tabs"; import { @@ -64,11 +54,13 @@ const SplitPane = SplitPane_ as any; type IEditorForDocumentProperties = { uid: any; - doc: IDocument | NonCloudFile; + doc: IDocument | IOpenDocument; projectUid: string; isOwner: boolean; }; +type AnyTab = IDocument | IOpenDocument | NonCloudFile; + const MySplit = ({ primary, split, @@ -78,11 +70,19 @@ const MySplit = ({ onDragStarted, onDragFinished, children -}) => { - /* eslint-disable-next-line unicorn/prefer-native-coercion-functions */ +}: { + primary: string; + split: string; + minSize: string; + maxSize: string; + defaultSize: string; + onDragStarted: () => void; + onDragFinished: () => void; + children: React.ReactNode[]; +}): React.ReactElement => { const filteredChildren = children.filter(Boolean); return filteredChildren.length === 1 ? ( - filteredChildren[0] + (filteredChildren[0] as React.ReactElement) ) : ( - ); - } else if ((doc as IDocument).type === "txt") { + if ((doc as IDocument).type === "txt") { return ( ); @@ -147,6 +116,17 @@ function EditorForDocument({ ) { const path = `${uid}/${projectUid}/${(doc as IDocument).documentUid}`; return ; + } else if ( + (doc as IOpenDocument).isNonCloudDocument && + (doc as IOpenDocument).nonCloudFileAudioUrl + ) { + return ( + + ); } return ( @@ -163,11 +143,11 @@ const MainSection = ({ tabDock: React.ReactElement; setIsDragging?: (isDragging: boolean) => void; }) => { - const openTabs: BottomTab[] | undefined = useSelector((store: IStore) => + const openTabs: BottomTab[] | undefined = useSelector((store: RootState) => selectOpenBottomTabs(store) ); - const bottomTabIndex = useSelector((store: IStore) => + const bottomTabIndex = useSelector((store: RootState) => selectBottomTabIndex(store) ); const showBottomTabs = !isEmpty(openTabs) && bottomTabIndex > -1; @@ -197,18 +177,22 @@ const ProjectEditor = ({ activeProject: IProject; }): React.ReactElement => { const dispatch = useDispatch(); - const theme: any = useTheme(); // The manual is an iframe, which doesn't detect // mouse positions, so we add an invidible layer then // resizing the manual panel. const [isDragging, setIsDragging] = useState(false); - const projectUid: string = propOr("", "projectUid", activeProject); - const projectOwnerUid: string = propOr("", "userUid", activeProject); - const isOwner: boolean = useSelector(selectIsOwner(projectUid)); - // eslint-disable-next-line unicorn/no-useless-undefined - const csound: CsoundObj | undefined = undefined; + const projectUid: string = activeProject?.projectUid ?? ""; + const projectOwnerUid: string = activeProject?.userUid ?? ""; + const projectName: string = activeProject?.name ?? "Undefined Project"; + const isOwner: boolean = useSelector(selectIsOwner); + + useEffect(() => { + if (document.title !== projectName) { + document.title = projectName; + } + }, [projectName]); useEffect(() => { // start at top on init @@ -221,14 +205,11 @@ const ProjectEditor = ({ useEffect(() => { const unsubscribeProjectChanges = subscribeToProjectChanges( - projectUid, - dispatch, - csound - ); - const unsubscribeToProjectLastModified = subscribeToProjectLastModified( projectUid, dispatch ); + const unsubscribeToProjectLastModified = + subscribeToProjectLastModified(projectUid); // get some metadata from other people's projects const unsubscribeToProfile = @@ -243,136 +224,79 @@ const ProjectEditor = ({ unsubscribeToProfile && unsubscribeToProfile(); unsubscribeToProjectsCount && unsubscribeToProjectsCount(); }; - }, [dispatch, isOwner, projectOwnerUid, projectUid, csound]); - - useEffect(() => { - dispatch(enableMidiInput()); - dispatch(enableAudioInput()); - }, [dispatch]); + }, [dispatch, isOwner, projectOwnerUid, projectUid]); const tabDockDocuments: IOpenDocument[] = useSelector( - pathOr([] as IOpenDocument[], [ - "ProjectEditorReducer", - "tabDock", - "openDocuments" - ]) - ); - - const nonCloudFiles: NonCloudFile[] = useSelector( - pathOr([] as NonCloudFile[], ["FileTreeReducer", "nonCloudFiles"]) + (store: RootState) => + store.ProjectEditorReducer?.tabDock?.openDocuments ?? + ([] as IOpenDocument[]) ); const tabIndex: number = useSelector( - pathOr(-1, ["ProjectEditorReducer", "tabDock", "tabIndex"]) + (store: RootState) => + store.ProjectEditorReducer?.tabDock?.tabIndex ?? -1 ); - const openDocuments: (IDocument | NonCloudFile)[] = reduce( - ( - accumulator: (IDocument | NonCloudFile)[], - tabDocument: IOpenDocument - ) => { - const maybeDocument = pathOr( - {} as IDocument, - ["documents", propOr("", "uid", tabDocument)], - activeProject - ); - - const maybeNonCloudFile = nonCloudFiles.find( - propEq("name", tabDocument.uid), - tabDocument.uid - ); - - return maybeNonCloudFile - ? append(maybeNonCloudFile, accumulator) + const openDocuments: AnyTab[] = tabDockDocuments.reduce( + (accumulator: AnyTab[], tabDocument: IOpenDocument) => { + const maybeDocument = activeProject.documents[tabDocument.uid]; + const isNonCloudFile = tabDocument.isNonCloudDocument || false; + + return isNonCloudFile + ? [...accumulator, tabDocument] : maybeDocument && Object.keys(maybeDocument).length > 0 - ? append(maybeDocument, accumulator) - : accumulator; + ? [...accumulator, maybeDocument] + : accumulator; }, - [] as (IDocument | NonCloudFile)[], - tabDockDocuments + [] as AnyTab[] ); - const closeTab = (documentUid, isModified) => { + const closeTab = (documentUid: string, isModified: boolean) => { dispatch(tabClose(projectUid, documentUid, isModified)); }; - const openTabList = mapIndexed((document: any, index) => { - const isActive: boolean = index === tabIndex; + const switchTab = (index: number) => { + localStorage.setItem(projectUid + ":tabIndex", `${index}`); + dispatch(tabSwitch(index)); + }; + + const openTabList = openDocuments.map((document: any, index) => { const isModified: boolean = document.isModifiedLocally; - const documentPathHuman = append( - document.filename, - (document.path || []).map((documentUid) => - pathOr( - "", - ["documents", documentUid, "filename"], - activeProject - ) - ) - ).join("/"); return ( - { - closeTab(document.documentUid, isModified); - }} - > - - - + closeCallback={() => + closeTab(document.documentUid || document.uid, isModified) } + currentIndex={tabIndex} + thisIndex={index} + CustomTabStyle={TabStyles.Tab} + handleTabChange={switchTab} + index={index} + active={index === tabIndex} > - 0 - ? documentPathHuman - : document.filename || document.name - : "" - } - > -

- {document - ? (document.filename || document.name) + - (isOwner && isModified ? "*" : "") - : ""} -

-
+

+ {document + ? (document.filename || document.name || document.uid) + + (isOwner && isModified ? "*" : "") + : ""} +

); }, openDocuments as IDocument[]); - const openTabPanels = mapIndexed( - (document_: any, index: number) => ( - - - - ), - openDocuments - ); - const switchTab = (index: number) => { - localStorage.setItem(projectUid + ":tabIndex", `${index}`); - dispatch(tabSwitch(index)); - }; + const openTabPanels = openDocuments.map((document_: any, index: number) => ( + + + + )); const someUnsavedData: boolean = isOwner ? !!find( @@ -392,14 +316,21 @@ const ProjectEditor = ({ const tabDock: React.ReactElement = isEmpty(openDocuments) ? (
) : ( -
+
{ + onTabSequenceChange={({ + oldIndex, + newIndex + }: { + oldIndex: number; + newIndex: number; + }) => { dispatch( rearrangeTabs( projectUid, @@ -409,18 +340,42 @@ const ProjectEditor = ({ ); }} > - {openTabList} + { + dispatch( + rearrangeTabs( + projectUid, + simpleSwitch( + tabDockDocuments, + oldIndex, + newIndex + ), + newIndex + ) + ); + }} + handleTabChange={switchTab} + > + {openTabList} + {openTabPanels}
); const isManualVisible = useSelector( - (store: IStore) => store.ProjectEditorReducer.manualVisible + (store: RootState) => store.ProjectEditorReducer.manualVisible ); const isFileTreeVisible = useSelector( - (store: IStore) => store.ProjectEditorReducer.fileTreeVisible + (store: RootState) => store.ProjectEditorReducer.fileTreeVisible ); useEffect(() => { @@ -437,8 +392,8 @@ const ProjectEditor = ({ useEffect(() => { if (projectUid) { - dispatch(storeProjectEditorKeyboardCallbacks(projectUid)); - dispatch(storeEditorKeyboardCallbacks(projectUid)); + storeProjectEditorKeyboardCallbacks(projectUid); + storeEditorKeyboardCallbacks(projectUid); } }, [dispatch, projectUid]); @@ -446,7 +401,9 @@ const ProjectEditor = ({ ) : ( <> @@ -462,7 +419,9 @@ const ProjectEditor = ({ onDragStarted={() => setIsDragging(true)} onDragFinished={() => setIsDragging(false)} > - {isFileTreeVisible && } + {isFileTreeVisible && ( + + )} ({ manualLookupString: "" }); -const addTabToOpenDocuments = curry((tab, state) => - over(lensPath(["tabDock", "openDocuments"]), append(tab), state) -); +const addTabToOpenDocuments = ( + tab: IOpenDocument, + state: IProjectEditorReducer +) => { + return { + ...state, + tabDock: { + ...state.tabDock, + openDocuments: [...state.tabDock.openDocuments, tab] + } + }; +}; const storeTabDockState = ( projectUid: string, @@ -56,89 +53,152 @@ const storeTabDockState = ( tabIndex: number | undefined ): void => { try { - const tabOrder: string[] = map(prop("uid"), openDocuments); + const tabOrder: string[] = openDocuments.map((doc) => doc.uid); const tabOrderString: string = JSON.stringify(tabOrder); localStorage.setItem(`${projectUid}:tabOrder`, tabOrderString); - tabIndex && + if (tabIndex !== undefined) { localStorage.setItem(`${projectUid}:tabIndex`, `${tabIndex}`); + } } catch (error) { console.error(error); } }; const ProjectEditorReducer = ( - state: IProjectEditorReducer, + state: IProjectEditorReducer | undefined, action: Record ): IProjectEditorReducer => { + if (!state) { + return initialLayoutState(); + } + switch (action.type) { case MANUAL_LOOKUP_STRING: { - return pipe( - assoc("manualLookupString", action.manualLookupString), - assoc("manualVisible", true) - )(state); + return { + ...state, + manualLookupString: action.manualLookupString, + manualVisible: true + }; } case TAB_DOCK_CLOSE: { return initialLayoutState(); } case TAB_DOCK_INIT: { - return pipe( - assocPath(["tabDock", "tabIndex"], action.initialIndex), - assocPath( - ["tabDock", "openDocuments"], - action.initialOpenDocuments - ) - )(state); + return { + ...state, + tabDock: { + ...state.tabDock, + tabIndex: action.initialIndex, + openDocuments: action.initialOpenDocuments + } + }; } case TAB_DOCK_SWITCH_TAB: { - return assocPath(["tabDock", "tabIndex"], action.tabIndex, state); + return { + ...state, + tabDock: { + ...state.tabDock, + tabIndex: action.tabIndex + } + }; } case TAB_DOCK_REARRANGE_TABS: { - return pipe( - assocPath(["tabDock", "tabIndex"], action.newActiveIndex), - assocPath(["tabDock", "openDocuments"], action.modifiedDock) - )(state); + return { + ...state, + tabDock: { + ...state.tabDock, + tabIndex: action.newActiveIndex, + openDocuments: action.modifiedDock + } + }; } - case TAB_DOCK_OPEN_TAB_BY_DOCUMENT_UID: { - const currentOpenDocuments: IOpenDocument[] = pathOr( - [] as IOpenDocument[], - ["tabDock", "openDocuments"], - state - ); + case TAB_DOCK_OPEN_NON_CLOUD_FILE: { + if (action.init) { + return state; + } + const currentOpenDocuments = state.tabDock.openDocuments; const documentAlreadyOpenIndex = findIndex( currentOpenDocuments, - (od: IOpenDocument) => od.uid === action.documentUid + (od) => + Boolean( + od.isNonCloudDocument && od.uid === action?.filename + ) ); - if (documentAlreadyOpenIndex < 0 || action.init) { - const newAppendedState = addTabToOpenDocuments( + const file = nonCloudFiles.get(action?.filename); + + if (file && documentAlreadyOpenIndex < 0) { + let nonCloudFileAudioUrl; + let nonCloudFileData; + + if (action.mimeType.startsWith("audio")) { + const blob = new Blob([file.buffer], { + type: action.mimeType + }); + nonCloudFileAudioUrl = URL.createObjectURL(blob); + } else { + const utf8decoder = new TextDecoder(); + nonCloudFileData = utf8decoder.decode(file.buffer); + } + + const newState = addTabToOpenDocuments( { - uid: action.documentUid, + uid: file.name, + isNonCloudDocument: true, + nonCloudFileAudioUrl, + nonCloudFileData, editorInstance: undefined }, state ); - // Focus on open action (can be made configureable) - const newState = assocPath( - ["tabDock", "tabIndex"], - newAppendedState.tabDock.openDocuments.length - 1, - newAppendedState - ); + newState.tabDock.tabIndex = + newState.tabDock.openDocuments.length - 1; - if (!action.isNonCloudDocument) { - storeTabDockState( - action.projectUid, - newState.tabDock.openDocuments, - newState.tabDock.tabIndex - ); - } + storeTabDockState( + action.projectUid, + newState.tabDock.openDocuments, + newState.tabDock.tabIndex + ); return newState; } else { - return assocPath( - ["tabDock", "tabIndex"], - documentAlreadyOpenIndex, + return { + ...state, + tabDock: { + ...state.tabDock, + tabIndex: documentAlreadyOpenIndex + } + }; + } + } + case TAB_DOCK_OPEN_TAB_BY_DOCUMENT_UID: { + const currentOpenDocuments = state.tabDock.openDocuments; + const documentAlreadyOpenIndex = findIndex( + currentOpenDocuments, + (od) => od.uid === action.documentUid + ); + + if (documentAlreadyOpenIndex < 0 || action.init) { + const newState = addTabToOpenDocuments( + { + uid: action.documentUid, + editorInstance: undefined + }, state ); + + newState.tabDock.tabIndex = + newState.tabDock.openDocuments.length - 1; + + return newState; + } else { + return { + ...state, + tabDock: { + ...state.tabDock, + tabIndex: documentAlreadyOpenIndex + } + }; } } case TAB_CLOSE: { @@ -147,72 +207,58 @@ const ProjectEditorReducer = ( (od) => od.uid === action.documentUid ) ) { - // dont attempt to close a tab that isn't open return state; } + const currentTabIndex = state.tabDock.tabIndex; - const newState: IProjectEditorReducer = (pipe as any)( - assocPath( - ["tabDock", "tabIndex"], - Math.min( - currentTabIndex, - pathOr( - 0, - ["tabDock", "openDocuments", "length"], - state - ) - 2 - ) - ), - assocPath( - ["tabDock", "openDocuments"], - filter( - (od: IOpenDocument) => od.uid !== action.documentUid, - pathOr([], ["tabDock", "openDocuments"], state) - ) - ) - )(state); + const newOpenDocuments = state.tabDock.openDocuments.filter( + (od) => od.uid !== action.documentUid + ); + + const newTabIndex = Math.min( + currentTabIndex, + newOpenDocuments.length - 1 + ); + + const newState = { + ...state, + tabDock: { + ...state.tabDock, + tabIndex: newTabIndex, + openDocuments: newOpenDocuments + } + }; + storeTabDockState( action.projectUid, newState.tabDock.openDocuments, newState.tabDock.tabIndex ); + return newState; } case TOGGLE_MANUAL_PANEL: { - return pipe( - assoc("manualLookupString", ""), - assoc("manualVisible", !state.manualVisible) - )(state); + return { + ...state, + manualLookupString: "", + manualVisible: !state.manualVisible + }; } case SET_MANUAL_PANEL_OPEN: { - return pipe( - assoc("manualLookupString", ""), - assoc("manualVisible", action.open) - )(state); + return { + ...state, + manualLookupString: "", + manualVisible: action.open + }; } case SET_FILE_TREE_PANEL_OPEN: { - return assoc("fileTreeVisible", action.open, state); - } - case STORE_EDITOR_INSTANCE: { - const openDocumentIndex = findIndex( - state.tabDock.openDocuments, - (od) => od.uid === action.documentUid - ); - return openDocumentIndex < 0 - ? state - : assocPath( - [ - "tabDock", - "openDocuments", - openDocumentIndex, - "editorInstance" - ], - action.editorInstance, - state - ); + return { + ...state, + fileTreeVisible: action.open + }; } default: { - return state || initialLayoutState(); + return state; } } }; diff --git a/src/components/project-editor/selectors.tsx b/src/components/project-editor/selectors.tsx index 9f1e7ad1..f3a8f7c4 100644 --- a/src/components/project-editor/selectors.tsx +++ b/src/components/project-editor/selectors.tsx @@ -1,26 +1,55 @@ -import { getAuth } from "firebase/auth"; -import { IStore } from "@store/types"; -import { curry, equals, path, pathOr } from "ramda"; +import { createSelector } from "@reduxjs/toolkit"; +import { RootState } from "@root/store"; +import { path, pathOr } from "ramda"; import { IOpenDocument } from "./types"; -export const selectIsOwner: (projectUid: string) => (store: IStore) => boolean = - curry((projectUid: string, store: IStore): boolean => { - const currentUser = getAuth().currentUser; - if (typeof currentUser !== "object") { - return false; +export const selectLoggedInUid = createSelector( + [ + (state: RootState) => { + if (!state.LoginReducer) return undefined; + return state.LoginReducer.loggedInUid; } - const owner = pathOr( - "", - ["ProjectsReducer", "projects", projectUid, "userUid"], - store - ); - return equals(owner, (currentUser && currentUser.uid) || -1); - }); + ], + (loggedInUid) => loggedInUid +); + +export const selectActiveProjectUid = createSelector( + [ + (state: RootState) => { + if (!state.ProjectsReducer) return undefined; + return state.ProjectsReducer.activeProjectUid; + } + ], + (activeProjectUid) => activeProjectUid +); + +export const selectProjectOwner = createSelector( + [ + selectActiveProjectUid, + (state: RootState) => { + return state.ProjectsReducer.projects; + } + ], + (projectUid: string | undefined, projects) => { + if (!projectUid || !projects) return undefined; + const project = projects[projectUid]; + return project?.userUid ?? undefined; + } +); + +export const selectIsOwner = createSelector( + [selectProjectOwner, selectLoggedInUid], + (ownerUid, loggedInUid) => { + return ownerUid === loggedInUid; + } +); -export const selectTabDockIndex = (store: IStore): number => +export const selectTabDockIndex = (store: RootState): number => pathOr(-1, ["ProjectEditorReducer", "tabDock", "tabIndex"], store); -export const selectCurrentTab = (store: IStore): IOpenDocument | undefined => { +export const selectCurrentTab = ( + store: RootState +): IOpenDocument | undefined => { const tabIndex = selectTabDockIndex(store); if (tabIndex > -1) { return path( @@ -31,19 +60,12 @@ export const selectCurrentTab = (store: IStore): IOpenDocument | undefined => { }; export const selectCurrentTabDocumentUid = ( - store: IStore + store: RootState ): string | undefined => { const tabIndex = selectTabDockIndex(store); if (tabIndex > -1) { - return path( - [ - "ProjectEditorReducer", - "tabDock", - "openDocuments", - tabIndex, - "uid" - ], - store - ); + return store?.ProjectEditorReducer?.tabDock?.openDocuments?.[tabIndex] + ?.uid; } + return undefined; }; diff --git a/src/components/project-editor/styles.ts b/src/components/project-editor/styles.ts index 8352d911..c16fc683 100644 --- a/src/components/project-editor/styles.ts +++ b/src/components/project-editor/styles.ts @@ -10,6 +10,10 @@ export const splitterRoot = css` padding-top: ${headerHeight}px; left: 0; box-sizing: border-box; + .Pane2 > div { + height: 100%; + width: 100%; + } `; export const closeButton = css` @@ -28,16 +32,15 @@ export const headIconsContainer = (theme: Theme): SerializedStyles => css` right: 16px; svg { - font-size: 16px; + font-size: 18px; color: ${theme.lineNumber}!important; } height: 36px; - // height: 20px; & span { cursor: pointer; &:hover { svg { - color: ${theme.textColor}!important; + fill: ${theme.textColor}!important; } } } @@ -69,18 +72,18 @@ export const mobileNavigationButtonAwesome = ( margin-bottom: 5px; `; -export const mobileConsole = (theme: Theme): SerializedStyles => css` +export const mobileConsole = css` height: calc(100vh - 130px); `; -export const mobileManual = (theme: Theme): SerializedStyles => css` +export const mobileManual = css` height: calc(100vh - 130px); & > div { padding: 0 !important; } `; -export const mobileFileTree = (theme: Theme): SerializedStyles => css` +export const mobileFileTree = css` zoom: 140%; & > div { padding: 0 !important; diff --git a/src/components/project-editor/tab-styles.tsx b/src/components/project-editor/tab-styles.tsx index f9c76b9f..3a7c0d14 100644 --- a/src/components/project-editor/tab-styles.tsx +++ b/src/components/project-editor/tab-styles.tsx @@ -1,17 +1,20 @@ -import React from "react"; import styled from "@emotion/styled"; -import { styled as themeStyled } from "@hlolli/react-tabtab"; +import { + TabListStyle, + ActionButtonStyle, + TabStyle, + PanelStyle +} from "@root/tabtab/index.js"; import { css, SerializedStyles, Theme } from "@emotion/react"; import { tabListHeight } from "@styles/constants"; import { _shadow } from "@styles/_common"; import { isMobile } from "@root/utils"; -let { TabListStyle, ActionButtonStyle, TabStyle, PanelStyle } = themeStyled; export const tabListStyle = (theme: Theme): SerializedStyles => css` & li::after { z-index: 0; } - ${!isMobile() ? "" : "display: none;"} + ${isMobile() ? "display: none;" : ""} & > div { height: 100%; @@ -28,8 +31,10 @@ export const tabListStyle = (theme: Theme): SerializedStyles => css` z-index: 1; background-color: ${theme.background}!important; height: ${tabListHeight}px; - box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), - 0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -2px rgba(0, 0, 0, 0.2); + box-shadow: + 0 2px 2px 0 rgba(0, 0, 0, 0.14), + 0 1px 5px 0 rgba(0, 0, 0, 0.12), + 0 3px 1px -2px rgba(0, 0, 0, 0.2); } .tablist > div { height: 100%; @@ -50,17 +55,19 @@ export const tabListStyle = (theme: Theme): SerializedStyles => css` } `; -TabListStyle = styled(TabListStyle)` +const TabListStyleCustom = styled(TabListStyle)` z-index: 1; background-color: ${(properties) => properties.theme.background}!important; bottom: 0; - box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), + box-shadow: + 0 2px 2px 0 rgba(0, 0, 0, 0.14), + 0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -2px rgba(0, 0, 0, 0.2); & li::after { z-index: 0; } - ${!isMobile() ? "" : "display: none;"} + ${isMobile() ? "display: none;" : ""} .tablist { width: 100%; @@ -84,19 +91,20 @@ TabListStyle = styled(TabListStyle)` } `; -TabStyle = styled(TabStyle)` +const TabStyleCustom = styled(TabStyle)` z-index: 1; position: relative; - color: ${(properties) => + color: ${(properties: any) => properties.active ? properties.theme.textColor : properties.theme.unfocusedTextColor}; - font-weight: ${(properties) => (properties.active ? 500 : 400)}; - background-color: ${(properties) => + font-weight: ${(properties: any) => (properties.active ? 500 : 400)}; + background-color: ${(properties: any) => properties.active ? properties.theme.highlightBackground : properties.theme.background}; border: 0; + font-size: 14px; padding: 0 !important; padding-right: 42px !important; padding-bottom: 2px !important; @@ -104,7 +112,7 @@ TabStyle = styled(TabStyle)` margin-bottom: -1px; &:hover { color: ${(properties) => properties.theme.textColor}!important; - background-color: ${(properties) => + background-color: ${(properties: any) => properties.active ? properties.theme.highlightBackground : properties.theme.highlightBackgroundAlt}; @@ -117,7 +125,7 @@ TabStyle = styled(TabStyle)` bottom: 0; right: 0; height: 3px; - background-color: ${(properties) => + background-color: ${(properties: any) => properties.active ? properties.theme.tabHighlightActive : properties.theme.tabHighlight}; @@ -128,7 +136,7 @@ TabStyle = styled(TabStyle)` } `; -ActionButtonStyle = styled(ActionButtonStyle)` +const ActionButtonStyleCustom = styled(ActionButtonStyle)` background-color: ${(properties) => properties.theme.gutterBackground}; border: 2px solid ${(properties) => properties.theme.line}; ${_shadow} @@ -152,29 +160,29 @@ const bottomPanelStyle = ` position: relative; `; -PanelStyle = styled(PanelStyle)` +const PanelStyleCustom = styled(PanelStyle)` width: 100%; height: 100%; - position: absolute !important; + //position: absolute !important; padding: 0 !important; padding-top: 38px !important; background-color: ${(properties) => properties.theme.background}!important; - ${(properties) => (properties.isBottom ? bottomPanelStyle : "")}; + ${(properties: any) => (properties.isBottom ? bottomPanelStyle : "")}; `; const TabStyles = (isBottom: boolean): Record => ({ - // eslint-disable-next-line react/display-name - TabList: (properties) => ( - + TabList: (properties: any) => ( + ), - // eslint-disable-next-line react/display-name - ActionButton: (properties) => ( - + ActionButton: (properties: any) => ( + ), - // eslint-disable-next-line react/display-name - Tab: (properties) => , - // eslint-disable-next-line react/display-name - Panel: (properties) => + Tab: (properties: any) => ( + + ), + Panel: (properties: any) => ( + + ) }); export default TabStyles; diff --git a/src/components/project-editor/types.ts b/src/components/project-editor/types.ts index 42796708..6b644e2a 100644 --- a/src/components/project-editor/types.ts +++ b/src/components/project-editor/types.ts @@ -7,18 +7,21 @@ export const SET_FILE_TREE_PANEL_OPEN = PREFIX + "SET_FILE_TREE_PANEL_OPEN"; export const TAB_DOCK_INIT = PREFIX + "TAB_DOCK_INIT"; export const TAB_DOCK_SWITCH_TAB = PREFIX + "TAB_DOCK_SWITCH_TAB"; export const TAB_DOCK_REARRANGE_TABS = PREFIX + "TAB_DOCK_REARRANGE_TABS"; +export const TAB_DOCK_OPEN_NON_CLOUD_FILE = + PREFIX + "TAB_DOCK_OPEN_NON_CLOUD_FILE"; export const TAB_DOCK_OPEN_TAB_BY_DOCUMENT_UID = PREFIX + "TAB_DOCK_OPEN_TAB_BY_DOCUMENT_UID"; export const TAB_DOCK_CLOSE = PREFIX + "TAB_DOCK_CLOSE"; export const TAB_CLOSE = PREFIX + "TAB_CLOSE"; export const TOGGLE_MANUAL_PANEL = PREFIX + "TOGGLE_MANUAL_PANEL"; -export const STORE_EDITOR_INSTANCE = PREFIX + "STORE_EDITOR_INSTANCE"; // DATA TYPES export interface IOpenDocument { - editorInstance: any; uid: string; isNonCloudDocument?: boolean; + nonCloudFileAudioUrl?: string | undefined; + nonCloudFileData?: string | undefined; + editorInstance?: any; } export interface ITabDock { diff --git a/src/components/project-editor/utils.tsx b/src/components/project-editor/utils.tsx index d8ef33e8..42b59428 100644 --- a/src/components/project-editor/utils.tsx +++ b/src/components/project-editor/utils.tsx @@ -1,4 +1,3 @@ -import { append, find, propEq, reduce } from "ramda"; import { IDocument } from "@comp/projects/types"; import { IOpenDocument } from "./types"; @@ -6,23 +5,22 @@ export const sortByStoredTabOrder = ( tabOrder: string[], allDocuments: IDocument[] ): IOpenDocument[] => { - return reduce( + return tabOrder.reduce( (accumulator: IOpenDocument[], documentUid: string) => { - const maybeDocument: IDocument | undefined = find( - propEq("documentUid", documentUid), - allDocuments + console.log("Document uid", allDocuments, documentUid); + const maybeDocument: IDocument | undefined = allDocuments.find( + (doc) => doc.documentUid === documentUid ); return maybeDocument - ? append( + ? [ + ...accumulator, { editorInstance: undefined, uid: maybeDocument.documentUid - } as IOpenDocument, - accumulator - ) + } as IOpenDocument + ] : accumulator; }, - [], - tabOrder + [] ); }; diff --git a/src/components/project-last-modified/actions.tsx b/src/components/project-last-modified/actions.tsx index 054167a0..634fedb4 100644 --- a/src/components/project-last-modified/actions.tsx +++ b/src/components/project-last-modified/actions.tsx @@ -1,9 +1,5 @@ import { doc, getDoc, setDoc } from "firebase/firestore"; -import { - getFirebaseTimestamp, - projectLastModified, - Timestamp -} from "@config/firestore"; +import { getFirebaseTimestamp, projectLastModified } from "@config/firestore"; import { UPDATE_PROJECT_LAST_MODIFIED_LOCALLY } from "./types"; export const updateProjectLastModified = async ( @@ -18,8 +14,8 @@ export const updateProjectLastModified = async ( export const updateProjectLastModifiedLocally = ( projectUid: string, - timestamp: Timestamp -): Record => ({ + timestamp: number +): { type: string; projectUid: string; timestamp: number } => ({ type: UPDATE_PROJECT_LAST_MODIFIED_LOCALLY, projectUid, timestamp @@ -33,7 +29,7 @@ export const getProjectLastModifiedOnce = ( doc(projectLastModified, projectUid) ); const timestampData = timestampReference.data() as any; - const timestamp = timestampData && timestampData.timestamp; + const timestamp = timestampData && timestampData.timestamp.toMillis(); return await dispatch( updateProjectLastModifiedLocally(projectUid, timestamp) ); diff --git a/src/components/project-last-modified/reducer.tsx b/src/components/project-last-modified/reducer.tsx index d7aebc02..994a8f35 100644 --- a/src/components/project-last-modified/reducer.tsx +++ b/src/components/project-last-modified/reducer.tsx @@ -1,9 +1,8 @@ -import { Timestamp } from "@config/firestore"; import { UPDATE_PROJECT_LAST_MODIFIED_LOCALLY } from "./types"; import { assocPath } from "ramda"; export interface IProjectLastModified { - timestamp: Timestamp | null; + timestamp: number | undefined; } export type IProjectLastModifiedReducer = { @@ -15,12 +14,15 @@ const ProjectLastModifiedReducer = ( action: Record ): IProjectLastModifiedReducer => { switch (action.type) { - case UPDATE_PROJECT_LAST_MODIFIED_LOCALLY: - return assocPath( - [action.projectUid, "timestamp"], - action.timestamp, - state - ); + case UPDATE_PROJECT_LAST_MODIFIED_LOCALLY: { + return { + ...state, + [action.projectUid]: { + ...(state?.[action.projectUid] || {}), + timestamp: action.timestamp + } + }; + } default: { return state || {}; } diff --git a/src/components/project-last-modified/selectors.ts b/src/components/project-last-modified/selectors.ts index c1f584a1..354fbe81 100644 --- a/src/components/project-last-modified/selectors.ts +++ b/src/components/project-last-modified/selectors.ts @@ -1,9 +1,14 @@ -import { IStore } from "@store/types"; -import { IProjectLastModified } from "./reducer"; -import { path } from "ramda"; +import { RootState } from "@root/store"; +import { createSelector } from "@reduxjs/toolkit"; -export const selectProjectLastModified = ( - projectUid: string -): ((store: IStore) => IProjectLastModified | undefined) => (store: IStore) => { - return path(["ProjectLastModifiedReducer", projectUid], store); -}; +export const selectProjectLastModified = (projectUid: string | undefined) => + createSelector( + [ + () => projectUid, + (state: RootState) => state.ProjectLastModifiedReducer + ], + (projectUid, projectLastModifiedReducer) => { + if (!projectUid) return undefined; + return projectLastModifiedReducer[projectUid]; + } + ); diff --git a/src/components/project-last-modified/subscribers.tsx b/src/components/project-last-modified/subscribers.tsx index f1936396..12b71009 100644 --- a/src/components/project-last-modified/subscribers.tsx +++ b/src/components/project-last-modified/subscribers.tsx @@ -1,10 +1,10 @@ import { doc, onSnapshot } from "firebase/firestore"; +import { store } from "@root/store"; import { projectLastModified } from "@config/firestore"; import { updateProjectLastModifiedLocally } from "./actions"; export const subscribeToProjectLastModified = async ( - projectUid: string, - dispatch: (any) => void + projectUid: string ): Promise<() => void> => { const unsubscribe: () => void = onSnapshot( doc(projectLastModified, projectUid), @@ -15,8 +15,11 @@ export const subscribeToProjectLastModified = async ( const { timestamp } = timestampDocument.data(); timestamp && - (await dispatch( - updateProjectLastModifiedLocally(projectUid, timestamp) + (await store.dispatch( + updateProjectLastModifiedLocally( + projectUid, + timestamp.toMillis() + ) )); }, (error: any) => console.error(error) diff --git a/src/components/projects/actions.tsx b/src/components/projects/actions.tsx index e479f5cb..f51c097a 100644 --- a/src/components/projects/actions.tsx +++ b/src/components/projects/actions.tsx @@ -1,27 +1,18 @@ -import { getAuth } from "firebase/auth"; -import { getDownloadURL, uploadBytesResumable } from "firebase/storage"; +import { AppThunkDispatch, RootState, store } from "@root/store"; +import { getDownloadURL } from "firebase/storage"; import { - addDoc, collection, - deleteDoc, doc, + DocumentData, + DocumentReference, getDoc, getDocs, updateDoc, writeBatch } from "firebase/firestore"; -import { push } from "connected-react-router/esm/index.js"; +import { push } from "connected-react-router"; import { CsoundObj } from "@csound/browser"; -import { - tabOpenByDocumentUid, - tabDockInit -} from "@comp/project-editor/actions"; -import { - addDocumentPrompt, - deleteDocumentPrompt, - newDocumentPrompt, - newFolderPrompt -} from "./modals"; +import { tabDockInit, closeTabDock } from "@comp/project-editor/actions"; import { selectDefaultTargetName, selectTarget @@ -29,9 +20,7 @@ import { import { selectLoggedInUid } from "@comp/login/selectors"; import { updateProjectLastModified } from "@comp/project-last-modified/actions"; import { selectTabDockIndex } from "@comp/project-editor/selectors"; -import { closeModal, openSimpleModal } from "../modal/actions"; -import { openSnackbar } from "@comp/snackbar/actions"; -import { SnackbarType } from "@comp/snackbar/types"; +import { openSimpleModal } from "../modal/actions"; import { filter as _filter, find, isEmpty } from "lodash"; import { append, @@ -39,10 +28,8 @@ import { concat, filter, forEach, - map, path, pathOr, - prop, reduce, values } from "ramda"; @@ -62,46 +49,118 @@ import { UNSET_PROJECT, IProject, IDocument, - IDocumentsMap + CsoundFile } from "./types"; import { ITarget } from "@comp/target-controls/types"; import { - addDocumentToEMFS, + addDocumentToCsoundFS, convertProjectSnapToProject, - fileDocumentDataToDocumentType, - textOrBinary + fileDocumentDataToDocumentType } from "./utils"; -import { IStore } from "@store/types"; import { database, getFirebaseTimestamp, projects, storageReference } from "@config/firestore"; -import { store } from "@root/store"; import JSZip from "jszip"; import { saveAs } from "file-saver"; -import { v4 as uuidv4 } from "uuid"; -import { Action } from "redux"; -import { ThunkAction } from "redux-thunk"; +import { IFirestoreDocument } from "@root/db/types"; + +// Track ongoing downloads to prevent duplicates +const ongoingDownloads = new Map>(); export const downloadProjectOnce = ( projectUid: string -): ((dispatch: any) => Promise) => { +): ((dispatch: any) => Promise<{ exists: boolean }>) => { + if (!projectUid) { + console.trace("[downloadProjectOnce] No projectUid provided"); + } return async (dispatch: any) => { - const projReference = doc(projects, projectUid); - let projSnap; - try { - projSnap = await getDoc(projReference); - } catch { - return; + if (!projectUid) { + console.trace( + "[downloadProjectOnce] Missing projectUid", + projectUid + ); + return { exists: false }; } - if (projSnap && projSnap.exists) { - const project: IProject = await convertProjectSnapToProject( - projSnap + if (!projects) { + console.trace( + "[downloadProjectOnce] Missing projects collection", + projects ); - await dispatch(storeProjectLocally([project])); + return { exists: false }; } + + // Check if download is already in progress + if (ongoingDownloads.has(projectUid)) { + return ongoingDownloads.get(projectUid)!; + } + + // Create and store the download promise + const downloadPromise = (async () => { + try { + let projReference: DocumentReference; + try { + projReference = doc(projects, projectUid); + } catch (error) { + console.error( + `[downloadProjectOnce] Error creating document reference:`, + error + ); + return { exists: false }; + } + + if (!projReference) { + console.trace( + "[downloadProjectOnce] Missing project reference", + projReference + ); + return { exists: false }; + } + + let projSnap: any; + try { + projSnap = await getDoc(projReference); + } catch (error) { + console.error( + `[downloadProjectOnce] Error fetching document from Firestore:`, + error + ); + return { exists: false }; + } + + if (projSnap && projSnap.exists()) { + const project: IProject = + await convertProjectSnapToProject(projSnap); + await dispatch(storeProjectLocally([project])); + + // Download project documents (files) as part of downloadProjectOnce + try { + await downloadAllProjectDocumentsOnce(projectUid)( + dispatch + ); + } catch (error) { + console.error( + `[downloadProjectOnce] Error downloading project documents:`, + error + ); + } + + return { exists: true }; + } else { + return { exists: false }; + } + } finally { + // Clean up the ongoing download tracking + ongoingDownloads.delete(projectUid); + } + })(); + + // Store the promise to prevent duplicates + ongoingDownloads.set(projectUid, downloadPromise); + + return downloadPromise; }; }; @@ -109,29 +168,46 @@ export const downloadAllProjectDocumentsOnce = ( projectUid: string ): ((dispatch: any) => Promise) => { return async (dispatch: any) => { - const filesReference = await getDocs( - collection(doc(projects, projectUid), "files") - ); - const allDocuments = await Promise.all( - filesReference.docs.map(async (d) => { - const data = await d.data(); - return fileDocumentDataToDocumentType( - assoc("documentUid", d.id, data) - ); - }) - ); - const allDocumentsMap = reduce( - (accumulator, document_) => - assoc(document_.documentUid, document_, accumulator), - {}, - allDocuments - ); + if (!projectUid) { + console.error( + "No projectUid provided to downloadAllProjectDocumentsOnce" + ); + return; + } - await dispatch({ - type: ADD_PROJECT_DOCUMENTS, - projectUid, - documents: allDocumentsMap - }); + try { + const filesReference = await getDocs( + collection(doc(projects, projectUid), "files") + ); + + const allDocuments = await Promise.all( + filesReference.docs.map(async (d) => { + const data = d.data() as IFirestoreDocument; + const document = fileDocumentDataToDocumentType( + { + ...data + }, + d.id + ); + return document; + }) + ); + const allDocumentsMap = reduce( + (accumulator, document_) => + assoc(document_.documentUid, document_, accumulator), + {}, + allDocuments + ); + + await dispatch({ + type: ADD_PROJECT_DOCUMENTS, + projectUid, + documents: allDocumentsMap + }); + } catch (error) { + console.error("Error downloading project documents:", error); + // Silently fail for non-existent projects + } }; }; @@ -139,23 +215,32 @@ export const newEmptyDocumentAction = ( projectUid: string, documentUid: string, filename: string -): Record => ({ +): { + type: typeof DOCUMENT_INITIALIZE; + filename: string; + documentUid: string; + projectUid: string; +} => ({ type: DOCUMENT_INITIALIZE, filename, documentUid, projectUid }); -export const closeProject = (): Record => { - return { - type: CLOSE_PROJECT +export const closeProject = () => { + return async (dispatch: AppThunkDispatch) => { + // Close tab dock when closing project + dispatch(closeTabDock()); + dispatch({ + type: CLOSE_PROJECT + }); }; }; -export const activateProject = ( - projectUid: string -): ((dispatch: any) => Promise) => { - return async (dispatch: any) => { +export const activateProject = (projectUid: string) => { + return async (dispatch: AppThunkDispatch) => { + // Close any existing tab dock before activating new project + dispatch(closeTabDock()); dispatch({ type: ACTIVATE_PROJECT, projectUid @@ -163,16 +248,18 @@ export const activateProject = ( }; }; -export const storeProjectLocally = ( - projects: Array -): Record => { +export const storeProjectLocally = (projects: Array) => { + const projectsWithoutTimestamps = projects.map((project) => ({ + ...project, + created: project.created?.toMillis() || undefined + })); return { type: STORE_PROJECT_LOCALLY, - projects + projects: projectsWithoutTimestamps }; }; -export const unsetProject = (projectUid: string): Record => { +export const unsetProject = (projectUid: string) => { return { type: UNSET_PROJECT, projectUid @@ -181,24 +268,24 @@ export const unsetProject = (projectUid: string): Record => { export const addProjectDocuments = ( projectUid: string, - documents: IDocumentsMap -): ((dispatch: any, getState: () => IStore) => Promise) => { - return async (dispatch: any, getState) => { - const store: IStore = getState(); + documents: Record +) => { + return async (dispatch: AppThunkDispatch, getState: () => RootState) => { + const store: RootState = getState(); const tabIndex: number = selectTabDockIndex(store); - await dispatch({ + dispatch({ type: ADD_PROJECT_DOCUMENTS, projectUid, documents }); if (tabIndex < 0) { const maybeDefaultTargetName: string | undefined = - selectDefaultTargetName(projectUid, store); + selectDefaultTargetName(projectUid)(store); + const maybeDefaultTarget: ITarget | undefined = selectTarget( projectUid, - maybeDefaultTargetName, - store - ); + maybeDefaultTargetName + )(store); dispatch( tabDockInit(projectUid, values(documents), maybeDefaultTarget) @@ -207,10 +294,7 @@ export const addProjectDocuments = ( }; }; -export const resetDocumentValue = ( - projectUid: string, - documentUid: string -): Record => { +export const resetDocumentValue = (projectUid: string, documentUid: string) => { return { type: DOCUMENT_RESET, projectUid, @@ -219,8 +303,8 @@ export const resetDocumentValue = ( }; export const saveFile = (): ((dispatch: any) => Promise) => { - return async (dispatch: any) => { - const state = store.getState() as IStore; + return async () => { + const state = store.getState() as RootState; const dock = state.ProjectEditorReducer.tabDock; const activeTab = dock.openDocuments[dock.tabIndex]; const documentUid = activeTab.uid; @@ -256,9 +340,9 @@ export const saveFile = (): ((dispatch: any) => Promise) => { }; }; -export const saveAllFiles = (): ((dispatch: any) => Promise) => { - return async (dispatch: any) => { - const state = store.getState() as IStore; +export const saveAllFiles = () => { + return async () => { + const state = store.getState() as RootState; const activeProjectUid = pathOr( "", ["ProjectsReducer", "activeProjectUid"], @@ -295,15 +379,8 @@ export const saveAllFiles = (): ((dispatch: any) => Promise) => { }; }; -export const saveAllAndClose = ( - goTo: string -): ((dispatch: any) => Promise) => { - return function (dispatch) { - return saveAllFiles()(dispatch).then( - () => dispatch(push(goTo)), - console.error - ); - }; +export const saveAllAndClose = (dispatch: any, goTo: string) => { + return saveAllFiles()().then(() => dispatch(push(goTo)), console.error); }; // for unauthorized or offline playing @@ -318,7 +395,7 @@ export const saveFileOffline = ( [`/${activeProjectUid}`], append(document.filename, pathPrefix) ).join("/"); - addDocumentToEMFS( + addDocumentToCsoundFS( activeProjectUid, csound, assoc("savedValue", newValue, document), @@ -328,7 +405,7 @@ export const saveFileOffline = ( // for unauthorized or offline playing export const saveAllOffline = (csound: CsoundObj): void => { - const state = store.getState() as IStore; + const state = store.getState() as RootState; const activeProjectUid = pathOr( "", ["ProjectsReducer", "activeProjectUid"], @@ -346,7 +423,7 @@ export const saveAllOffline = (csound: CsoundObj): void => { [`/${activeProjectUid}`], append(document.filename, pathPrefix) ).join("/"); - addDocumentToEMFS( + addDocumentToCsoundFS( activeProjectUid, csound, assoc("savedValue", document.currentValue, document), @@ -359,9 +436,9 @@ export const saveAllOffline = (csound: CsoundObj): void => { export const deleteFile = ( projectUid: string, documentUid: string -): ((dispatch: any, getState: () => IStore) => Promise) => { +): ((dispatch: any, getState: () => RootState) => Promise) => { return async (dispatch: any, getState) => { - const state = getState() as IStore; + const state = getState() as RootState; const project: IProject = pathOr( {} as IProject, ["ProjectsReducer", "projects", projectUid], @@ -370,65 +447,33 @@ export const deleteFile = ( if (project) { const document_ = project.documents[documentUid]; if (document_ && document_.type === "folder") { - const cancelCallback = () => dispatch(closeModal()); const allNestedFiles = filter( - (d) => (d.path || []).includes(documentUid), + (d: IDocument) => (d.path || []).includes(documentUid), project.documents ); - const allNestedFilenames = map( - prop("filename"), + const allFilesToDelete = append( + document_, values(allNestedFiles) ); - const deleteCallback = () => { - const allFilesToDelete = append( - document_, - values(allNestedFiles) - ); - const batch = writeBatch(database); - allFilesToDelete.forEach((document__) => { - batch.delete( - doc( - collection( - doc(projects, project.projectUid), - "files" - ), - document__.documentUid - ) - ); - }); - batch.commit().then(() => { - dispatch(closeModal()); - updateProjectLastModified(projectUid); - }); - }; - const deleteDocumentPromptComp = deleteDocumentPrompt( - document_.filename, - true, - allNestedFilenames, - cancelCallback, - deleteCallback + dispatch( + openSimpleModal("delete-document-prompt", { + filename: document_.filename, + isFolder: true, + folderContents: allFilesToDelete, + documentUid, + projectUid + }) ); - dispatch(openSimpleModal(deleteDocumentPromptComp, {})); } else if (document_) { - const cancelCallback = async () => await dispatch(closeModal()); - const deleteCallback = async () => { - await deleteDoc( - doc( - collection(doc(projects, projectUid), "files"), - documentUid - ) - ); - await updateProjectLastModified(projectUid); - }; - const deleteDocumentPromptComp = deleteDocumentPrompt( - document_.filename, - false, - [], - cancelCallback, - deleteCallback + dispatch( + openSimpleModal("delete-document-prompt", { + filename: document_.filename, + isFolder: false, + folderContents: [], + documentUid, + projectUid + }) ); - - dispatch(openSimpleModal(deleteDocumentPromptComp, {})); } else { console.error("No document found with id", projectUid); } @@ -477,242 +522,30 @@ export const updateDocumentModifiedLocally = ( }; }; -export const newFolder = ( - projectUid: string -): ((dispatch: any, getState: () => IStore) => Promise) => { - return async (dispatch: any, getState) => { - const state = store.getState() as IStore; - const project: IProject = pathOr( - {} as IProject, - ["ProjectsReducer", "projects", projectUid], - state - ); - const userUid: string = pathOr( - {} as IProject, - ["LoginReducer", "loggedInUid"], - state - ); - const newFolderSuccessCallback = async (filename: string) => { - if (!isEmpty(project)) { - const document_ = { - type: "folder", - name: filename, - userUid, - path: [], - lastModified: getFirebaseTimestamp(), - created: getFirebaseTimestamp() - }; - await addDoc( - collection(doc(projects, projectUid), "files"), - document_ - ); - updateProjectLastModified(projectUid); - } - dispatch(closeModal()); - }; - const newFolderPromptComp = newFolderPrompt( - newFolderSuccessCallback, - project - ); - dispatch(openSimpleModal(newFolderPromptComp, {})); - }; +export const newFolder = (projectUid: string) => { + return openSimpleModal("new-folder-prompt", { projectUid }); }; -export const newDocument = ( - projectUid: string, - value: string -): ((dispatch: any) => Promise) => { - return async (dispatch: any) => { - const newDocumentSuccessCallback = async (filename: string) => { - const state = store.getState() as IStore; - const activeProjectUid = pathOr( - "", - ["ProjectsReducer", "activeProjectUid"], - state - ); - const project: IProject = pathOr( - {} as IProject, - ["ProjectsReducer", "projects", activeProjectUid], - state - ); - - if (!isEmpty(project)) { - const currentUser = getAuth().currentUser; - const uid = currentUser ? currentUser.uid : ""; - const document_ = { - type: "txt", - name: filename, - value: value, - userUid: uid, - lastModified: getFirebaseTimestamp(), - created: getFirebaseTimestamp(), - path: [] - }; - const result = await addDoc( - collection(doc(projects, project.projectUid), "files"), - document_ - ); - - const documentUid = result.id; - dispatch(tabOpenByDocumentUid(result.id, projectUid)); - dispatch( - newEmptyDocumentAction(projectUid, documentUid, filename) - ); - updateProjectLastModified(project.projectUid); - } - dispatch(closeModal()); - }; - const newDocumentPromptComp = newDocumentPrompt( - newDocumentSuccessCallback, - false, - "" - ); - dispatch(openSimpleModal(newDocumentPromptComp, {})); - }; +export const newDocument = (projectUid: string, initFilename: string) => { + return openSimpleModal("new-document-prompt", { + isRenameAction: false, + initFilename, + projectUid + }); }; -export const addDocument = ( - projectUid: string -): ((dispatch: any) => Promise) => { - return async (dispatch: any) => { - const addDocumentSuccessCallback = async (files: FileList) => { - const state = store.getState() as IStore; - const activeProjectUid = pathOr( - "", - ["ProjectsReducer", "activeProjectUid"], - state - ); - const project: IProject = pathOr( - {} as IProject, - ["ProjectsReducer", "projects", activeProjectUid], - state - ); - - if (!isEmpty(project) && files && files.length > 0) { - const file = files[0]; - const filename = file.name; - const fileType = textOrBinary(file.name); - const reader = new FileReader(); - const currentUser = getAuth().currentUser; - const uid = currentUser ? currentUser.uid : ""; - - console.log("File type found:", fileType); - - if (fileType === "txt") { - reader.addEventListener("load", async () => { - const txt = reader.result; - const document_ = { - type: fileType, - name: filename, - value: txt, - userUid: uid, - lastModified: getFirebaseTimestamp(), - created: getFirebaseTimestamp() - }; - - const result = await addDoc( - collection( - doc(projects, project.projectUid), - "files" - ), - document_ - ); - - const documentUid = result.id; - dispatch( - tabOpenByDocumentUid(documentUid, activeProjectUid) - ); - dispatch( - newEmptyDocumentAction( - project.projectUid, - documentUid, - filename - ) - ); - updateProjectLastModified(project.projectUid); - }); - reader.readAsText(file); - } else if (fileType === "bin") { - // generate UUID - const documentId = uuidv4(); - - const metadata = { - customMetadata: { - filename, - projectUid, - userUid: uid, - docUid: documentId - } - }; - - const uploadTask = uploadBytesResumable( - await storageReference( - `${uid}/${project.projectUid}/${documentId}` - ), - file, - metadata - ); - - // Listen for state changes, errors, and completion of the upload. - uploadTask.on( - "state_changed", - (snapshot) => { - // Get task progress, including the number of bytes uploaded and the total number of bytes to be uploaded - const progress = - (snapshot.bytesTransferred / - snapshot.totalBytes) * - 100; - console.log("Upload is " + progress + "% done"); - switch (snapshot.state) { - case "paused": - console.log("Upload is paused"); - break; - case "running": - console.log("Upload is running"); - break; - } - }, - function (error: any) { - dispatch( - openSnackbar(error.message, SnackbarType.Error) - ); - // A full list of error codes is available at - // https://firebase.google.com/docs/storage/web/handle-errors - switch (error.name) { - case "storage/unauthorized": - // User doesn't have permission to access the object - break; - - case "storage/canceled": - // User canceled the upload - break; - - case "storage/unknown": - // Unknown error occurred, inspect error.serverResponse - break; - } - }, - function () { - // Upload completed successfully, now we can get the download URL - // uploadTask.snapshot.ref.getDownloadURL() - // cloud function updates firestore for file entry - } - ); - } - } - dispatch(closeModal()); - }; - const addDocumentPromptComp = addDocumentPrompt( - addDocumentSuccessCallback - ); - dispatch(openSimpleModal(addDocumentPromptComp, {})); - }; +export const addDocument = (projectUid: string) => { + return openSimpleModal("add-document-prompt", { projectUid }); }; -const renameDocumentLocally = ( +export const renameDocumentLocally = ( documentUid: string, newFilename: string -): Record => { +): { + type: typeof DOCUMENT_RENAME_LOCALLY; + newFilename: string; + documentUid: string; +} => { return { type: DOCUMENT_RENAME_LOCALLY, newFilename, @@ -733,57 +566,24 @@ export const removeDocumentLocally = ( export const renameDocument = ( projectUid: string, - documentUid: string -): ((dispatch: any, getState: () => IStore) => Promise) => { - return async (dispatch: any, getState) => { - const state = getState() as IStore; - const project: IProject = pathOr( - {} as IProject, - ["ProjectsReducer", "projects", projectUid], - state - ); - if (!project) { - console.error("Project", projectUid, "was not found!"); - return; - } - const currentFilename = pathOr( - "", - ["documents", documentUid, "filename"], - project - ); - const renameDocumentSuccessCallback = async (filename: string) => { - await updateDoc( - doc( - collection(doc(projects, projectUid), "files"), - documentUid - ), - { name: filename } as any - ); - - dispatch(renameDocumentLocally(documentUid, filename)); - updateProjectLastModified(projectUid); - dispatch(closeModal()); - }; - const renameDocumentPromptComp = newDocumentPrompt( - renameDocumentSuccessCallback, - true, - currentFilename - ); - dispatch(openSimpleModal(renameDocumentPromptComp, {})); + documentUid: string, + newFilename: string +): ((dispatch: any) => Promise) => { + return async (dispatch: any) => { + dispatch(renameDocumentLocally(documentUid, newFilename)); }; }; -const createExportPath = (folders, document_): string => { - if (!folders || pathOr([], ["path"], document_).length === 0) { - return document_.filename; - } - const paths = document_.path.map((d) => folders[d].filename); - return `${paths.join("/")}/${document_.filename}`; +const createExportPath = (document: IDocument, projectUid: string): string => { + const pathPrefix = document.path || []; + return concat([projectUid], append(document.filename, pathPrefix)).join( + "/" + ); }; export const exportProject = (): ((dispatch: any) => Promise) => { - return async (dispatch: any) => { - const state = store.getState() as IStore; + return async () => { + const state = store.getState() as RootState; const activeProjectUid = pathOr( "", ["ProjectsReducer", "activeProjectUid"], @@ -794,67 +594,55 @@ export const exportProject = (): ((dispatch: any) => Promise) => { ["ProjectsReducer", "projects", activeProjectUid], state ); - - if (!isEmpty(project)) { + if (project) { const zip = new JSZip(); - const folder = zip.folder("project") as any; - const documents = Object.values(project.documents); - - const folders = documents - .filter((d) => d.type === "folder") - /* eslint-disable unicorn/prefer-object-from-entries */ - .reduce((m, f) => { - return { ...m, [f.documentUid]: f }; - }, {}); - - if (!folders) { - console.error(`No folders found.`); - return; - } - for (const document_ of documents) { - if (document_.type === "bin") { - const path = `${project.userUid}/${project.projectUid}/${document_.documentUid}`; - const url = await getDownloadURL( - await storageReference(path) + const documents: IDocument[] = Object.values(project.documents); + for (const document of documents) { + if (document.type === "txt") { + zip.file( + createExportPath(document, project.projectUid), + document.currentValue ); - - const response = await fetch(url); - const blob = await response.arrayBuffer(); - const exportPath = createExportPath(folders, document_); - if (exportPath && folder && folder.file) { - folder.file(exportPath, blob, { binary: true }); - } else { - console.error(`whoops, no export path was created`); + } else if (document.type === "bin") { + const path = `${document.userUid}/${project.projectUid}/${document.documentUid}`; + try { + const downloadUrl = await getDownloadURL( + await storageReference(path) + ); + const response = await fetch(downloadUrl); + const arrayBuffer = await response.arrayBuffer(); + zip.file( + createExportPath(document, project.projectUid), + arrayBuffer + ); + } catch (error) { + console.error(error); } - } else if (document_.type === "txt") { - const exportPath = createExportPath(folders, document_); - folder.file(exportPath, document_.savedValue); } } - - zip.generateAsync({ type: "blob" }).then((content) => { - saveAs(content, "project.zip"); - }); + const content = await zip.generateAsync({ type: "blob" }); + saveAs(content, `${project.name}.zip`); } }; }; -export const markProjectPublic = ( - projectUid: string, - isPublic: boolean -): ThunkAction> => { - return async (dispatch, getState) => { +export const markProjectPublic = (projectUid: string, isPublic: boolean) => { + return async (dispatch: AppThunkDispatch, getState: () => RootState) => { const state = getState(); - const loggedInUserUid = selectLoggedInUid(state); - if (!loggedInUserUid || !projectUid) { - return; + const loggedInUid = selectLoggedInUid(state); + if (loggedInUid) { + try { + await updateDoc(doc(projects, projectUid), { + public: isPublic + }); + dispatch({ + type: SET_PROJECT_PUBLIC, + projectUid, + isPublic + }); + } catch (error) { + console.error("Error updating project public status:", error); + } } - await updateDoc(doc(projects, projectUid), { public: isPublic }); - dispatch({ - type: SET_PROJECT_PUBLIC, - projectUid, - isPublic - }); - updateProjectLastModified(projectUid); }; }; diff --git a/src/components/projects/modals.tsx b/src/components/projects/modals.tsx index b1a38cbc..7b79560c 100644 --- a/src/components/projects/modals.tsx +++ b/src/components/projects/modals.tsx @@ -1,249 +1,468 @@ -import React, { useState } from "react"; +import React, { useCallback, useState } from "react"; +import { getAuth } from "firebase/auth"; +import { uploadBytesResumable } from "firebase/storage"; import { useSelector } from "react-redux"; -import TextField from "@material-ui/core/TextField"; -import Button from "@material-ui/core/Button"; +import { v4 as uuidv4 } from "uuid"; +import TextField from "@mui/material/TextField"; +import Button from "@mui/material/Button"; import { pathOr, isEmpty, values } from "ramda"; import { some } from "lodash"; import { formatFileSize } from "@root/utils"; +import { updateProjectLastModified } from "@comp/project-last-modified/actions"; +import { + addDoc, + collection, + deleteDoc, + doc, + updateDoc, + writeBatch +} from "firebase/firestore"; +import { + database, + getFirebaseTimestamp, + projects, + storageReference +} from "@config/firestore"; +import { openSnackbar } from "@comp/snackbar/actions"; +import { SnackbarType } from "@comp/snackbar/types"; +import { tabOpenByDocumentUid } from "@comp/project-editor/actions"; +import { closeModal } from "../modal/actions"; +import { newEmptyDocumentAction, renameDocumentLocally } from "./actions"; import * as SS from "./styles"; import { IProject, IDocument } from "./types"; +import { textOrBinary } from "./utils"; +import { useDispatch } from "@root/store"; -export const deleteDocumentPrompt = ( - filename: string, - isFolder: boolean, - folderContents: string[], - cancelCallback: () => void, - deleteCallback: () => void -): (() => React.ReactElement) => { - return function DeleteDocumentPrompt() { - return ( -
-

- {isFolder - ? `Delete${ - folderContents.length === 0 ? " empty" : "" - } directory ${filename}?` - : "Confirm deletion of file: " + filename} -

- {folderContents.length > 0 && ( - <> -

{`Warning! Deleting directory ${filename} will also permenantly delete the following files:`}

-
    - {folderContents.map((item, index) => ( -
  • {item}
  • - ))} -
- - )} - - - -
+export function DeleteDocumentPrompt({ + filename, + isFolder, + folderContents, + documentUid, + projectUid +}: { + filename: string; + isFolder: boolean; + folderContents: IDocument[]; + documentUid: string; + projectUid: string; +}) { + const dispatch = useDispatch(); + const cancelCallback = () => dispatch(closeModal()); + const deleteFileCallback = useCallback(async () => { + await deleteDoc( + doc(collection(doc(projects, projectUid), "files"), documentUid) ); - }; -}; + await updateProjectLastModified(projectUid); + dispatch(closeModal()); + }, [projectUid, documentUid, dispatch]); + + const deleteFolderCallback = useCallback(async () => { + const batch = writeBatch(database); + folderContents.forEach((document__) => { + batch.delete( + doc( + collection(doc(projects, projectUid), "files"), + document__.documentUid + ) + ); + }); + batch.commit().then(() => { + dispatch(closeModal()); + updateProjectLastModified(projectUid); + }); + }, [projectUid, dispatch, folderContents]); + + return ( +
+

+ {isFolder + ? `Delete${ + folderContents.length === 0 ? " empty" : "" + } directory ${filename}?` + : "Confirm deletion of file: " + filename} +

+ {folderContents.length > 0 && ( + <> +

{`Warning! Deleting directory ${filename} will also permenantly delete the following files:`}

+
    + {folderContents.map((item, index) => ( +
  • {item.filename}
  • + ))} +
+ + )} + + + +
+ ); +} // https://stackoverflow.com/questions/11100821/javascript-regex-for-validating-filenames const isValidFolderName = (name: string) => - // eslint-disable-next-line no-useless-escape !/^(con|prn|aux|nul|com\d|lpt\d)$|(["*/:<>?\\|])|([\s.])$/i.test(name); -export const newFolderPrompt = ( - callback: (fileName: string) => void, - project: IProject -): (() => React.ReactElement) => { - return function NewFolderPrompt() { - const [input, setInput] = useState(""); - const [nameCollides, setNameCollides] = useState(false); - - const reservedFilenames = project.documents - ? (values(project.documents) as IDocument[]).map( - (document_) => document_.filename - ) - : []; - - const shouldDisable = isEmpty(input) || !isValidFolderName(input); - - return ( -
- - event_.key === "Enter" && - !shouldDisable && - callback(input) +export function NewFolderPrompt({ projectUid }: { projectUid: string }) { + const [input, setInput] = useState(""); + const [nameCollides, setNameCollides] = useState(false); + const dispatch = useDispatch(); + const project: IProject = useSelector( + pathOr({} as IProject, ["ProjectsReducer", "projects", projectUid]) + ); + + const reservedFilenames = project.documents + ? (values(project.documents) as IDocument[]).map( + (document_) => document_.filename + ) + : []; + + const newFolderSuccessCallback = useCallback(async () => { + if (!isEmpty(project)) { + const currentUser = getAuth().currentUser; + const uid = currentUser ? currentUser.uid : ""; + const document_ = { + type: "folder", + name: input, + userUid: uid, + path: [], + lastModified: getFirebaseTimestamp(), + created: getFirebaseTimestamp() + }; + await addDoc( + collection(doc(projects, projectUid), "files"), + document_ + ); + updateProjectLastModified(projectUid); + } + dispatch(closeModal()); + }, [dispatch, input, project, projectUid]); + + const shouldDisable = isEmpty(input) || !isValidFolderName(input); + + return ( +
+ + event_.key === "Enter" && + !shouldDisable && + newFolderSuccessCallback() + } + error={nameCollides} + value={input} + onChange={(event) => { + setInput(event.target.value); + setNameCollides( + some( + reservedFilenames, + (function_) => function_ === event.target.value + ) + ); + }} + /> + +
+ ); +} +export function NewDocumentPrompt({ + isRenameAction, + initFilename, + renameDocumentUid, + projectUid +}: { + isRenameAction: boolean; + initFilename: string; + renameDocumentUid?: string; + projectUid: string; +}) { + const [input, setInput] = useState(isRenameAction ? initFilename : ""); + const [nameCollides, setNameCollides] = useState(false); + const dispatch = useDispatch(); + + const project: IProject = useSelector( + pathOr({} as IProject, ["ProjectsReducer", "projects", projectUid]) + ); + + const reservedFilenames = project.documents + ? (values(project.documents) as IDocument[]).map( + (document_) => document_.filename + ) + : []; + + const shouldDisable = isEmpty(input); + + const newDocumentSuccessCallback = useCallback(async () => { + if (!isEmpty(project)) { + const currentUser = getAuth().currentUser; + const uid = currentUser ? currentUser.uid : ""; + const document_ = { + type: "txt", + name: input, + value: "", + userUid: uid, + lastModified: getFirebaseTimestamp(), + created: getFirebaseTimestamp(), + path: [] + }; + const result = await addDoc( + collection(doc(projects, project.projectUid), "files"), + document_ + ); + + const documentUid = result.id; + dispatch(tabOpenByDocumentUid(result.id, projectUid)); + dispatch(newEmptyDocumentAction(projectUid, documentUid, input)); + updateProjectLastModified(project.projectUid); + dispatch(closeModal()); + } + dispatch(closeModal()); + }, [projectUid, input, dispatch, project]); + + const renameDocumentSuccessCallback = useCallback(async () => { + if (renameDocumentUid) { + await updateDoc( + doc( + collection(doc(projects, projectUid), "files"), + renameDocumentUid + ), + { name: input } as any + ); + + dispatch(renameDocumentLocally(renameDocumentUid, input)); + updateProjectLastModified(projectUid); + dispatch(closeModal()); + } + }, [dispatch, input, projectUid, renameDocumentUid]); + + return ( +
+ + // event_.key === "Enter" && !shouldDisable && isRenameAction + // ? renameDocumentSuccessCallback() + // : newDocumentSuccessCallback() + // } + error={nameCollides} + value={input} + onChange={(event) => { + setInput(event.target.value); + setNameCollides( + some( + reservedFilenames, + (function_) => function_ === event.target.value + ) + ); + }} + /> + +
+ ); +} + +export function AddDocumentPrompt({ projectUid }: { projectUid: string }) { + const dispatch = useDispatch(); + const [files, setFiles] = useState(null as FileList | null); + + const [nameCollides, setNameCollides] = useState(false); + + const project: IProject = useSelector( + pathOr({} as IProject, ["ProjectsReducer", "projects", projectUid]) + ); + + const addDocumentSuccessCallback = useCallback(async () => { + if (!isEmpty(project) && files !== null && files.length > 0) { + const file = files[0]; + const filename = file.name; + const fileType = textOrBinary(file.name); + const reader = new FileReader(); + const currentUser = getAuth().currentUser; + const uid = currentUser ? currentUser.uid : ""; + + console.log("File type found:", fileType); + + if (fileType === "txt") { + reader.addEventListener("load", async () => { + const txt = reader.result; + const document_ = { + type: fileType, + name: filename, + value: txt, + userUid: uid, + lastModified: getFirebaseTimestamp(), + created: getFirebaseTimestamp() + }; + + const result = await addDoc( + collection(doc(projects, project.projectUid), "files"), + document_ + ); + + const documentUid = result.id; + dispatch(tabOpenByDocumentUid(documentUid, projectUid)); + dispatch( + newEmptyDocumentAction( + projectUid, + documentUid, + filename + ) + ); + updateProjectLastModified(project.projectUid); + }); + reader.readAsText(file); + } else if (fileType === "bin") { + // generate UUID + const documentId = uuidv4(); + + const metadata = { + customMetadata: { + filename, + projectUid, + userUid: uid, + docUid: documentId } - error={nameCollides} - value={input} - onChange={(event) => { - setInput(event.target.value); - setNameCollides( - some( - reservedFilenames, - (function_) => function_ === event.target.value - ) + }; + + const uploadTask = uploadBytesResumable( + await storageReference( + `${uid}/${project.projectUid}/${documentId}` + ), + file, + metadata + ); + + // Listen for state changes, errors, and completion of the upload. + uploadTask.on( + "state_changed", + (snapshot) => { + const progress = + (snapshot.bytesTransferred / snapshot.totalBytes) * + 100; + console.log("Upload is " + progress + "% done"); + switch (snapshot.state) { + case "paused": { + console.log("Upload is paused"); + break; + } + case "running": { + console.log("Upload is running"); + break; + } + } + }, + function (error: any) { + dispatch( + openSnackbar(error.message, SnackbarType.Error) ); - }} - /> - -
- ); - }; -}; - -export const newDocumentPrompt = ( - callback: (fileName: string) => void, - renameAction: boolean, - initFilename: string -): (() => React.ReactElement) => { - return function NewDocumentPrompt() { - const [input, setInput] = useState(renameAction ? initFilename : ""); - const [nameCollides, setNameCollides] = useState(false); - - const activeProjectUid = useSelector( - pathOr("", ["ProjectsReducer", "activeProjectUid"]) - ); - const project: IProject = useSelector( - pathOr({} as IProject, [ - "ProjectsReducer", - "projects", - activeProjectUid - ]) - ); + // A full list of error codes is available at + // https://firebase.google.com/docs/storage/web/handle-errors + switch (error.name) { + case "storage/unauthorized": { + // User doesn't have permission to access the object + break; + } - const reservedFilenames = project.documents - ? (values(project.documents) as IDocument[]).map( - (document_) => document_.filename - ) - : []; - - const shouldDisable = isEmpty(input); - return ( -
- - event_.key === "Enter" && - !shouldDisable && - callback(input) + case "storage/canceled": { + // User canceled the upload + break; + } + + case "storage/unknown": { + // Unknown error occurred, inspect error.serverResponse + break; + } + } + }, + function () { + // Upload completed successfully, now we can get the download URL + // uploadTask.snapshot.ref.getDownloadURL() + // cloud function updates firestore for file entry } - error={nameCollides} - value={input} + ); + } + } + dispatch(closeModal()); + }, [dispatch, files, projectUid, project]); + + const reservedFilenames = (values(project.documents) as IDocument[]).map( + (document_) => document_.filename + ); + + const megabyte_limit = Math.pow(10, 6) * 2; + const shouldDisable = + !files || isEmpty(files) || files[0].size > megabyte_limit; + const filesize = files ? formatFileSize(files[0].size) : "Select file"; + return ( +
+ -
- ); - }; -}; - -export const addDocumentPrompt = ( - callback: (filelist: FileList) => void -): (() => React.ReactElement) => { - return function AddDocumentPrompt() { - const [files, setFiles]: [ - FileList | undefined | null, - (files: FileList | null) => void - ] = useState(); - const [nameCollides, setNameCollides] = useState(false); - - const activeProjectUid = useSelector( - pathOr("", ["ProjectsReducer", "activeProjectUid"]) - ); - const project: IProject = useSelector( - pathOr({} as IProject, [ - "ProjectsReducer", - "projects", - activeProjectUid - ]) - ); - - const reservedFilenames = (values( - project.documents - ) as IDocument[]).map((document_) => document_.filename); - - const megabyte_limit = Math.pow(10, 6) * 2; - const shouldDisable = - !files || isEmpty(files) || files[0].size > megabyte_limit; - const filesize = !files ? "Select file" : formatFileSize(files[0].size); - return ( -
- -

{`File Size: ${filesize} (Max file size is 2MB)`}

- -
- ); - }; -}; + > + +

{`File Size: ${filesize} (Max file size is 2MB)`}

+ +
+ ); +} diff --git a/src/components/projects/project-context.tsx b/src/components/projects/project-context.tsx index 1a179988..ab86d99d 100644 --- a/src/components/projects/project-context.tsx +++ b/src/components/projects/project-context.tsx @@ -1,120 +1,139 @@ -import React, { useEffect, useState } from "react"; -import { Path } from "history"; +import { useEffect, useState } from "react"; import { Audio as AudioSpinner } from "react-loader-spinner"; -import { useParams } from "react-router-dom"; -import { push } from "connected-react-router/esm/index.js"; -import { useTheme } from "@emotion/react"; +import { useParams, useNavigate } from "react-router"; +import { Theme, useTheme } from "@emotion/react"; // import { IStore } from "@store/types"; import { useSelector, useDispatch } from "react-redux"; import ProjectEditor from "@comp/project-editor/project-editor"; import { IProject } from "@comp/projects/types"; import { cleanupNonCloudFiles } from "@comp/file-tree/actions"; -import Header from "@comp/header/header"; -import { activateProject, downloadProjectOnce } from "./actions"; +import { Header } from "@comp/header/header"; +import { activateProject, downloadProjectOnce, closeProject } from "./actions"; +import { isEmpty, pathOr } from "ramda"; +import { RootState } from "@root/store"; import * as SS from "./styles"; -import { isEmpty, path, pathOr } from "ramda"; -interface IProjectContextProperties { - match: any; -} - -const ForceBackgroundColor = ({ theme }): React.ReactElement => ( +const ForceBackgroundColor = ({ theme }: { theme: Theme }) => ( ); -const ProjectContext = ( - properties: IProjectContextProperties -): React.ReactElement => { +export const ProjectContext = () => { const dispatch = useDispatch(); + const navigate = useNavigate(); const theme = useTheme(); const routeParams: { id?: string } = useParams(); const [projectFetchStarted, setProjectFetchStarted] = useState(false); const [projectIsReady, setProjectIsReady] = useState(false); const [needsLoading, setNeedsLoading] = useState(true); - const projectUid = routeParams.id ? routeParams.id : ""; + const projectUid = routeParams.id ?? ""; const invalidUrl = !projectUid || isEmpty(projectUid); // this is true when /editor path is missing projectUid - invalidUrl && - dispatch( - push({ pathname: "/404" } as Path, { message: "Project not found" }) - ); + if (invalidUrl) { + navigate("/404", { + state: { message: "Project not found" } + }); + } const activeProjectUid: string | undefined = useSelector( - (store) => - !invalidUrl && path(["ProjectsReducer", "activeProjectUid"], store) + (store: RootState) => + !invalidUrl ? store?.ProjectsReducer?.activeProjectUid : undefined ); - const project: IProject | undefined = useSelector( - (store) => - activeProjectUid && - !invalidUrl && - path(["ProjectsReducer", "projects", activeProjectUid], store) + const project: IProject | undefined = useSelector((store: RootState) => + activeProjectUid && !invalidUrl + ? store?.ProjectsReducer?.projects?.[activeProjectUid] + : undefined ); const tabIndex: number = useSelector( pathOr(-1, ["ProjectEditorReducer", "tabDock", "tabIndex"]) ); + // Effect 1: Reset states when projectUid changes + useEffect(() => { + setProjectFetchStarted(false); + setProjectIsReady(false); + setNeedsLoading(true); + }, [projectUid]); + + // Effect 2: Handle project download initiation useEffect(() => { - if (!projectFetchStarted) { - const initProject = async () => { + if (!projectFetchStarted && projectUid) { + setProjectFetchStarted(true); + + const downloadProject = async () => { try { - await downloadProjectOnce(projectUid)(dispatch); + const result = + await downloadProjectOnce(projectUid)(dispatch); + if (!result.exists) { + setProjectIsReady(true); + navigate("/404", { + state: { message: "Project not found" } + }); + return; + } } catch (error: any) { + console.error( + `[ProjectContext] Error during project download:`, + error + ); + setProjectIsReady(true); if ( typeof error === "object" && typeof error.code === "string" ) { error.code === "permission-denied" && - dispatch( - push({ pathname: "/404" } as Path, { - message: "Project not found" - }) - ); + navigate("/404", { + state: { message: "Project not found" } + }); } + return; } - dispatch(cleanupNonCloudFiles()); + + // Cleanup and activate project + dispatch( + cleanupNonCloudFiles({ + projectUid + }) as any + ); await activateProject(projectUid)(dispatch); setProjectIsReady(true); }; - setProjectFetchStarted(true); - initProject(); + + downloadProject(); } + }, [projectUid]); // Only depend on projectUid, not on projectFetchStarted or dispatch + // Effect 3: Handle loading state management + useEffect(() => { if (needsLoading && projectFetchStarted && projectIsReady) { setNeedsLoading(false); } - }, [ - dispatch, - project, - projectUid, - activeProjectUid, - needsLoading, - projectIsReady, - projectFetchStarted, - tabIndex - ]); + }, [needsLoading, projectFetchStarted, projectIsReady]); - return !needsLoading && !invalidUrl && project ? ( - <> - - -
- - ) : ( + // Effect 4: Cleanup when component unmounts (user navigates away from project editor) + useEffect(() => { + return () => { + // Clean up tab dock when leaving project editor entirely + dispatch(closeProject() as any); + }; + }, [dispatch]); + + return ( <> + {project && }
-
- -
+ {needsLoading && ( +
+ +
+ )} ); }; - -export default ProjectContext; diff --git a/src/components/projects/reducer.ts b/src/components/projects/reducer.ts index 5d5086d0..87cc79bd 100644 --- a/src/components/projects/reducer.ts +++ b/src/components/projects/reducer.ts @@ -1,264 +1,359 @@ -import { - IProjectsReducer, - IProject, - ACTIVATE_PROJECT, - ADD_PROJECT_DOCUMENTS, - DOCUMENT_INITIALIZE, - DOCUMENT_RESET, - DOCUMENT_RENAME_LOCALLY, - DOCUMENT_REMOVE_LOCALLY, - DOCUMENT_SAVE, - DOCUMENT_UPDATE_VALUE, - DOCUMENT_UPDATE_MODIFIED_LOCALLY, - CLOSE_PROJECT, - SET_PROJECT_PUBLIC, - STORE_PROJECT_LOCALLY, - STORE_PROJECT_STARS, - UNSET_PROJECT -} from "./types"; -import { UPDATE_PROJECT_LAST_MODIFIED_LOCALLY } from "@comp/project-last-modified/types"; +import { RootState } from "@root/store"; +import * as ProjectsTypes from "./types"; import { generateEmptyDocument } from "./utils"; -import { - assoc, - assocPath, - curry, - dissoc, - dissocPath, - mergeAll, - hasPath, - pathOr, - pipe, - reduce -} from "ramda"; -import { isEmpty } from "lodash"; - -type IProjectMap = { [projectUid: string]: IProject }; - -const initialProjectsState: IProjectsReducer = { + +type IProjectMap = { [projectUid: string]: ProjectsTypes.IProject }; + +const initialProjectsState: ProjectsTypes.IProjectsReducer = { activeProjectUid: "", projects: {} as IProjectMap }; -const resetDocumentToSavedValue = curry( - (state: IProjectsReducer, activeProjectUid: string, documentUid: string) => - pipe( - (st) => - assocPath( - [ - "projects", - activeProjectUid, - "documents", - documentUid, - "currentValue" - ], - pathOr( - "", - [ - "projects", - activeProjectUid, - "documents", - documentUid, - "savedValue" +const resetDocumentToSavedValue = ( + state: ProjectsTypes.IProjectsReducer, + activeProjectUid: string, + documentUid: string +) => { + const savedValue = + state?.projects?.[activeProjectUid]?.documents?.[documentUid] + ?.savedValue || ""; + + return { + ...state, + projects: { + ...state.projects, + [activeProjectUid]: { + ...state.projects[activeProjectUid], + documents: { + ...state.projects[activeProjectUid].documents, + [documentUid]: { + ...state.projects[activeProjectUid].documents[ + documentUid ], - st - ), - st - ), - assocPath( - [ - "projects", - activeProjectUid, - "documents", - documentUid, - "isModifiedLocally" - ], - false - ) - )(state) -); - -const ProjectsReducer = ( - state: IProjectsReducer | undefined, - action: Record -): IProjectsReducer => { - if (!state) { - return initialProjectsState; - } else { - switch (action.type) { - case STORE_PROJECT_LOCALLY: { - if (isEmpty(action.projects)) { - return state; + currentValue: savedValue, + isModifiedLocally: false + } } - - const newState = reduce( - (st, proj) => { - const path = ["projects", proj.projectUid]; - return hasPath(path, st) - ? assocPath( - path, - mergeAll([ - pathOr({}, path, st), - pipe( - dissoc("tags"), - dissoc("stars"), - dissoc("documents") - )(proj) - ]), - st - ) - : assocPath(path, proj, st); - }, - state, - action.projects - ); - return newState; - } - case UNSET_PROJECT: { - return dissocPath(["projects", action.projectUid], state); - } - case ADD_PROJECT_DOCUMENTS: { - const path = ["projects", action.projectUid, "documents"]; - return assocPath( - path, - mergeAll([pathOr({}, path, state), action.documents]) - )(state) as IProjectsReducer; - } - case SET_PROJECT_PUBLIC: { - return assocPath( - ["projects", action.projectUid, "isPublic"], - action.isPublic - )(state) as IProjectsReducer; - } - case ACTIVATE_PROJECT: - return assoc("activeProjectUid", action.projectUid, state); - case CLOSE_PROJECT: { - return dissoc("activeProjectUid", state); } - case STORE_PROJECT_STARS: { - return assocPath( - ["projects", action.projectUid, "stars"], - action.stars, - state - ); + } + }; +}; + +export const ProjectsReducer = ( + state: ProjectsTypes.IProjectsReducer = initialProjectsState, + unknownAction: ProjectsTypes.ProjectsActionTypes +): ProjectsTypes.IProjectsReducer => { + switch (unknownAction.type) { + case ProjectsTypes.STORE_PROJECT_LOCALLY: { + const action = + unknownAction as ProjectsTypes.StoreProjectLocallyAction; + if (!action.projects || action.projects.length === 0) { + return state; } - case DOCUMENT_INITIALIZE: { - const newDocument = generateEmptyDocument( - action.documentUid, - action.filename + + const newState = action.projects.reduce((st, proj) => { + const existingProject = st.projects?.[proj.projectUid] || {}; + + return st.projects?.[proj.projectUid] + ? { + ...st, + projects: { + ...st.projects, + [proj.projectUid]: { + ...existingProject, + ...Object.entries(proj).reduce( + (acc: any, [key, value]) => { + if ( + ![ + "tags", + "stars", + "documents" + ].includes(key) + ) { + acc[key] = value; + } + return acc; + }, + {} + ) + } + } + } + : { + ...st, + projects: { + ...st.projects, + [proj.projectUid]: proj + } + }; + }, state); + + return newState; + } + + case ProjectsTypes.UNSET_PROJECT: { + const action = unknownAction as ProjectsTypes.UnsetProjectAction; + const { + [action.projectUid]: removedProject, + ...remainingProjects + } = state.projects; + return { + ...state, + projects: remainingProjects + }; + } + + case ProjectsTypes.ADD_PROJECT_DOCUMENTS: { + const action = + unknownAction as ProjectsTypes.AddProjectDocumentsAction; + + // Ensure the project exists before trying to add documents + if (!state.projects[action.projectUid]) { + console.error( + `[ProjectsReducer] Cannot add documents to non-existent project: ${action.projectUid}` ); - return assocPath( - [ - "projects", - action.projectUid, - "documents", - action.documentUid - ], - newDocument - )(state) as IProjectsReducer; - } - case DOCUMENT_REMOVE_LOCALLY: { - return dissocPath([ - "projects", - action.projectUid, - "documents", - action.documentUid - ])(state); - } - case DOCUMENT_RESET: { - return state - ? resetDocumentToSavedValue( - state, - action.projectUid, - action.documentUid - ) - : state; + return state; } - case DOCUMENT_SAVE: { - const path = [ - "projects", - action.projectUid, - "documents", - action.document.documentUid - ]; - return assocPath( - path, - action.document - )(state) as IProjectsReducer; - } - case DOCUMENT_UPDATE_VALUE: { - return !action.documentUid || !action.projectUid || !state - ? state - : (pipe( - assocPath( - [ - "projects", - action.projectUid, - "documents", - action.documentUid, - "isModifiedLocally" - ], - action.val !== - state.projects[action.projectUid].documents[ - action.documentUid - ].savedValue - ), - assocPath( - [ - "projects", - action.projectUid, - "documents", - action.documentUid, - "currentValue" - ], - action.val - ) - )(state) as IProjectsReducer); - } - case DOCUMENT_UPDATE_MODIFIED_LOCALLY: { - if (!action.documentUid || !state) { - return state; + + return { + ...state, + projects: { + ...state.projects, + [action.projectUid]: { + ...state.projects[action.projectUid], + documents: { + ...(state.projects[action.projectUid].documents || + {}), + ...action.documents + } + } } - return assocPath( - [ - "projects", - action.projectUid, - "documents", - action.documentUid, - "isModifiedLocally" - ], - action.isModified - )(state) as IProjectsReducer; - } - case DOCUMENT_RENAME_LOCALLY: { - return assocPath( - [ - "projects", - state.activeProjectUid, - "documents", - action.documentUid, - "filename" - ], - action.newFilename - )(state); - } - case UPDATE_PROJECT_LAST_MODIFIED_LOCALLY: { - return state && - action.projectUid !== null && - action.projectUid === state.activeProjectUid - ? (assocPath as any)( - [ - "projects", - state.activeProjectUid, - "cachedProjectLastModified" - ], - action.timestamp, - state as IProjectsReducer - ) - : state; - } - default: { - return (state as IProjectsReducer) || initialProjectsState; + }; + } + + case ProjectsTypes.SET_PROJECT_PUBLIC: { + const action = + unknownAction as ProjectsTypes.SetProjectPublicAction; + return { + ...state, + projects: { + ...state.projects, + [action.projectUid]: { + ...state.projects[action.projectUid], + isPublic: action.isPublic + } + } + }; + } + + case ProjectsTypes.ACTIVATE_PROJECT: { + const action = unknownAction as ProjectsTypes.ActivateProjectAction; + return { ...state, activeProjectUid: action.projectUid }; + } + + case ProjectsTypes.CLOSE_PROJECT: { + const action = unknownAction as ProjectsTypes.CloseProjectAction; + const { activeProjectUid, ...remainingState } = state; + return remainingState; + } + + case ProjectsTypes.STORE_PROJECT_STARS: { + const action = + unknownAction as ProjectsTypes.StoreProjectStarsAction; + return { + ...state, + projects: { + ...state.projects, + [action.projectUid]: { + ...state.projects[action.projectUid], + stars: action.stars + } + } + }; + } + + case ProjectsTypes.DOCUMENT_INITIALIZE: { + const action = + unknownAction as ProjectsTypes.DocumentInitializeAction; + const newDocument = generateEmptyDocument( + action.documentUid, + action.filename + ); + return { + ...state, + projects: { + ...state.projects, + [action.projectUid]: { + ...state.projects[action.projectUid], + documents: { + ...state.projects[action.projectUid].documents, + [action.documentUid]: newDocument + } + } + } + }; + } + + case ProjectsTypes.DOCUMENT_REMOVE_LOCALLY: { + const action = + unknownAction as ProjectsTypes.DocumentRemoveLocallyAction; + const { + [action.documentUid]: removedDocument, + ...remainingDocuments + } = state.projects[action.projectUid].documents; + return { + ...state, + projects: { + ...state.projects, + [action.projectUid]: { + ...state.projects[action.projectUid], + documents: remainingDocuments + } + } + }; + } + + case ProjectsTypes.DOCUMENT_RESET: { + const action = unknownAction as ProjectsTypes.DocumentResetAction; + return state + ? resetDocumentToSavedValue( + state, + action.projectUid, + action.documentUid + ) + : state; + } + + case ProjectsTypes.DOCUMENT_SAVE: { + const action = unknownAction as ProjectsTypes.DocumentSaveAction; + + return { + ...state, + projects: { + ...state.projects, + [action.projectUid]: { + ...state.projects[action.projectUid], + documents: { + ...state.projects[action.projectUid].documents, + [action.document.documentUid]: action.document + } + } + } + }; + } + + case ProjectsTypes.DOCUMENT_UPDATE_VALUE: { + const action = + unknownAction as ProjectsTypes.DocumentUpdateValueAction; + + if (!action.documentUid || !action.projectUid || !state) + return state; + + const isModifiedLocally = + action.val !== + state.projects[action.projectUid].documents[action.documentUid] + .savedValue; + + return { + ...state, + projects: { + ...state.projects, + [action.projectUid]: { + ...state.projects[action.projectUid], + documents: { + ...state.projects[action.projectUid].documents, + [action.documentUid]: { + ...state.projects[action.projectUid].documents[ + action.documentUid + ], + isModifiedLocally, + currentValue: action.val + } + } + } + } + }; + } + + case ProjectsTypes.DOCUMENT_UPDATE_MODIFIED_LOCALLY: { + const action = + unknownAction as ProjectsTypes.DocumentUpdateModifiedLocallyAction; + + if (!action.documentUid || !state) return state; + + return { + ...state, + projects: { + ...state.projects, + [action.projectUid]: { + ...state.projects[action.projectUid], + documents: { + ...state.projects[action.projectUid].documents, + [action.documentUid]: { + ...state.projects[action.projectUid].documents[ + action.documentUid + ], + isModifiedLocally: action.isModified + } + } + } + } + }; + } + + case ProjectsTypes.DOCUMENT_RENAME_LOCALLY: { + const action = + unknownAction as ProjectsTypes.DocumentRenameLocallyAction; + + return typeof state.activeProjectUid !== "string" + ? state + : { + ...state, + projects: { + ...state.projects, + [state.activeProjectUid]: { + ...state.projects[state.activeProjectUid], + documents: { + ...state.projects[state.activeProjectUid] + .documents, + [action.documentUid]: { + ...state.projects[state.activeProjectUid] + .documents[action.documentUid], + filename: action.newFilename + } + } + } + } + }; + } + + case ProjectsTypes.UPDATE_PROJECT_LAST_MODIFIED_LOCALLY: { + const action = + unknownAction as ProjectsTypes.UpdateProjectLastModifiedLocallyAction; + + if ( + state && + action.projectUid !== null && + action.projectUid === state.activeProjectUid + ) { + return typeof state.activeProjectUid !== "string" + ? state + : { + ...state, + projects: { + ...state.projects, + [state.activeProjectUid]: { + ...state.projects[state.activeProjectUid], + cachedProjectLastModified: action.timestamp + } + } + }; } + return state; + } + + default: { + return state; } } }; diff --git a/src/components/projects/selectors.ts b/src/components/projects/selectors.ts index 85bb7c39..505ac251 100644 --- a/src/components/projects/selectors.ts +++ b/src/components/projects/selectors.ts @@ -1,12 +1,9 @@ -import { IStore } from "@store/types"; +import { RootState } from "@root/store"; import { IProject, IProjectsReducer } from "./types"; -import { path, prop } from "ramda"; -export const selectActiveProject = (store: IStore): IProject | undefined => { +export const selectActiveProject = (store: RootState): IProject | undefined => { const state: IProjectsReducer = store.ProjectsReducer; - const activeProjectUid: string | undefined = prop( - "activeProjectUid", - state - ); - return activeProjectUid && path(["projects", activeProjectUid], state); + const activeProjectUid = state?.activeProjectUid; + + return activeProjectUid ? state.projects?.[activeProjectUid] : undefined; }; diff --git a/src/components/projects/subscribers.tsx b/src/components/projects/subscribers.tsx index d066def5..62315e4e 100644 --- a/src/components/projects/subscribers.tsx +++ b/src/components/projects/subscribers.tsx @@ -1,10 +1,10 @@ import { collection, doc, onSnapshot } from "firebase/firestore"; -import { store } from "@store/index"; +import { store, RootState, AppThunkDispatch } from "@root/store"; import { CsoundObj } from "@csound/browser"; import { projects, targets } from "@config/firestore"; -import { IDocument, IDocumentsMap } from "./types"; +import { IDocument } from "./types"; import { - addDocumentToEMFS, + addDocumentToCsoundFS, convertDocumentSnapToDocumentsMap, fileDocumentDataToDocumentType } from "./utils"; @@ -15,205 +15,154 @@ import { } from "./actions"; import { tabClose } from "@comp/project-editor/actions"; import { updateAllTargetsLocally } from "@comp/target-controls/actions"; -import { - append, - assoc, - concat, - filter, - forEach, - map, - isEmpty, - path, - prop, - propEq, - values -} from "ramda"; +import { append, concat, forEach, isEmpty, path, values } from "ramda"; +import { IFirestoreDocument } from "@root/db/types"; export const subscribeToProjectFilesChanges = ( projectUid: string, - dispatch: (any) => void, - csound: CsoundObj | undefined -): (() => void) => { - const unsubscribe: () => void = onSnapshot( - collection(doc(projects, projectUid), "files"), - async (files) => { - const changedFiles = files.docChanges(); - const filesToAdd = filter(propEq("type", "added"), changedFiles); - const filesToRemove = filter( - propEq("type", "removed"), - changedFiles - ); - const filesToModify = filter( - propEq("type", "modified"), - changedFiles - ); - - if (!isEmpty(filesToAdd)) { - const documents: IDocumentsMap = - convertDocumentSnapToDocumentsMap(filesToAdd); - csound && - forEach((d) => { - if (d.type !== "folder") { - const pathPrefix = (d.path || []) - .filter((p) => typeof p === "string") - .map((documentUid) => - path([documentUid, "filename"], documents) - ); - const absolutePath = concat( - [`/${projectUid}`], - append(d.filename, pathPrefix) - ).join("/"); - addDocumentToEMFS( - projectUid, - csound, - d, - absolutePath - ); - } - }, values(documents)); - dispatch(addProjectDocuments(projectUid, documents)); - } + dispatch: AppThunkDispatch +) => { + if (!projectUid) { + console.warn( + "No projectUid provided to subscribeToProjectFilesChanges" + ); + return () => {}; // Return empty unsubscribe function + } - if (!isEmpty(filesToModify)) { - const currentReduxDocuments = path( - ["ProjectsReducer", "projects", projectUid, "documents"], - store.getState() + try { + const unsubscribe: () => void = onSnapshot( + collection(doc(projects, projectUid), "files"), + async (files) => { + const changedFiles = files.docChanges(); + const filesToAdd = changedFiles.filter( + (file) => file.type === "added" ); - const documentSnaps = map(prop("doc"), filesToModify); - const documentData = map( - (d) => - fileDocumentDataToDocumentType( - assoc("documentUid", d.id, d.data()) - ), - documentSnaps - ) as IDocument[]; - - // when using serverData, we get 2 responses, - // since we always modify the lastModified timestamp, - // it will be null the first time and immedietly not-null - const documentDataReady = documentData.filter( - (d) => !!d.lastModified + const filesToRemove = changedFiles.filter( + (file) => file.type === "removed" ); - await documentDataReady.forEach(async (document_) => { - if (document_.type !== "folder") { - const oldFile = - currentReduxDocuments[document_.documentUid]; - const lastPathPrefix = (oldFile.path || []) - .filter((p) => typeof p === "string") - .map((documentUid) => - path( - [documentUid, "filename"], - currentReduxDocuments - ) - ); - const lastAbsolutePath = concat( - [`/${projectUid}`], - append(oldFile.filename, lastPathPrefix) - ).join("/"); - const newPathPrefix = (document_.path || []) - .filter((p) => typeof p === "string") - .map((documentUid) => - path( - [documentUid, "filename"], - currentReduxDocuments - ) - ); - const newAbsolutePath = concat( - [`/${projectUid}`], - append(document_.filename, newPathPrefix) - ).join("/"); - // Handle file moved - if (newAbsolutePath !== lastAbsolutePath) { - csound && - (await csound.fs.unlink(lastAbsolutePath)); - } else { - csound && (await csound.fs.unlink(newAbsolutePath)); - } - csound && - addDocumentToEMFS( - projectUid, - csound, - document_, - newAbsolutePath - ); - dispatch(saveUpdatedDocument(projectUid, document_)); - } - }); - } - if (!isEmpty(filesToRemove)) { - const currentReduxDocuments = path( - ["ProjectsReducer", "projects", projectUid, "documents"], - store.getState() + const filesToModify = changedFiles.filter( + (file) => file.type === "modified" ); - const documentSnaps = map(prop("doc"), filesToRemove); - const documentData = map( - (d) => - fileDocumentDataToDocumentType( - assoc("documentUid", d.id, d.data()) - ), - documentSnaps - ) as IDocument[]; - const uids = map(prop("documentUid"), documentData); - uids.forEach((uid) => { - dispatch(tabClose(projectUid, uid, false)); - dispatch(removeDocumentLocally(projectUid, uid)); - }); - await documentData.forEach(async (document_) => { - if (document_.type !== "folder") { - const pathPrefix = (document_.path || []) - .filter((p) => typeof p === "string") - .map((documentUid) => - path( - [documentUid, "filename"], - currentReduxDocuments - ) + if (!isEmpty(filesToAdd)) { + const documents: Record = + convertDocumentSnapToDocumentsMap(filesToAdd); + await dispatch(addProjectDocuments(projectUid, documents)); + } + + if (!isEmpty(filesToModify)) { + const currentReduxDocuments = + store.getState()?.ProjectsReducer?.projects?.[ + projectUid + ]?.documents || {}; + + const documentSnaps = filesToModify.map((file) => file.doc); + + const documentData = documentSnaps.map((d) => { + const firestoreData = d.data() as IFirestoreDocument; + return fileDocumentDataToDocumentType( + firestoreData, + d.id + ); + }) as IDocument[]; + + // when using serverData, we get 2 responses, + // since we always modify the lastModified timestamp, + // it will be null the first time and immedietly not-null + const documentDataReady = documentData.filter( + (d) => !!d.lastModified + ); + documentDataReady.forEach(async (document_) => { + if (document_.type !== "folder") { + await dispatch( + saveUpdatedDocument( + projectUid, + document_ + ) as any ); - const absolutePath = concat( - [`/${projectUid}`], - append(document_.filename, pathPrefix) - ).join("/"); - csound && (await csound.fs.unlink(absolutePath)); + } + }); + } + if (!isEmpty(filesToRemove)) { + const currentReduxDocuments = + store.getState()?.ProjectsReducer?.projects?.[ + projectUid + ]?.documents || {}; + + const documentSnaps = filesToRemove.map((file) => file.doc); + + const documentData = documentSnaps.map((d) => { + const firestoreData = d.data() as IFirestoreDocument; + return fileDocumentDataToDocumentType( + firestoreData, + d.id + ); + }) as IDocument[]; + + const uids = documentData.map((d) => d.documentUid); + + for (const uid of uids) { + await dispatch(tabClose(projectUid, uid, false) as any); + await dispatch( + removeDocumentLocally(projectUid, uid) as any + ); } - }); + } + }, + (error: any) => { + console.error("Error in project files subscription:", error); } - } - ); - return unsubscribe; + ); + return unsubscribe; + } catch (error) { + console.error("Error setting up project files subscription:", error); + return () => {}; // Return empty unsubscribe function + } }; export const subscribeToProjectTargetsChanges = ( projectUid: string, - dispatch: (any) => void + dispatch: (store: RootState) => void ): (() => void) => { - const unsubscribe: () => void = onSnapshot( - doc(targets, projectUid), - async (target) => { - if (!target.exists()) { - return; - } - const { defaultTarget, targets } = await target.data(); - updateAllTargetsLocally( - dispatch, - defaultTarget, - projectUid, - targets - ); - }, - (error: any) => console.error(error) - ); - return unsubscribe; + if (!projectUid) { + console.warn( + "No projectUid provided to subscribeToProjectTargetsChanges" + ); + return () => {}; // Return empty unsubscribe function + } + + try { + const unsubscribe: () => void = onSnapshot( + doc(targets, projectUid), + async (target) => { + if (!target.exists()) { + return; + } + const { defaultTarget, targets } = target.data(); + updateAllTargetsLocally( + dispatch, + defaultTarget, + projectUid, + targets + ); + }, + (error: any) => + console.error("Error in project targets subscription:", error) + ); + return unsubscribe; + } catch (error) { + console.error("Error setting up project targets subscription:", error); + return () => {}; // Return empty unsubscribe function + } }; export const subscribeToProjectChanges = ( projectUid: string, - dispatch: (any) => void, - csound: CsoundObj | undefined + dispatch: AppThunkDispatch ): (() => void) => { const unsubscribeFileChanges = subscribeToProjectFilesChanges( projectUid, - dispatch, - csound + dispatch ); const unsubscribeTargetChanges = subscribeToProjectTargetsChanges( projectUid, diff --git a/src/components/projects/types.ts b/src/components/projects/types.ts index 2f0a2eec..1e625204 100644 --- a/src/components/projects/types.ts +++ b/src/components/projects/types.ts @@ -1,4 +1,6 @@ -import { Timestamp } from "@config/firestore"; +import { Timestamp } from "firebase/firestore"; +import { UPDATE_PROJECT_LAST_MODIFIED_LOCALLY } from "../project-last-modified/types"; +export { UPDATE_PROJECT_LAST_MODIFIED_LOCALLY } from "../project-last-modified/types"; const PREFIX = "PROJECTS."; @@ -19,15 +21,120 @@ export const DOCUMENT_UPDATE_VALUE = PREFIX + "DOCUMENT_UPDATE_VALUE"; export const DOCUMENT_UPDATE_MODIFIED_LOCALLY = PREFIX + "DOCUMENT_UPDATE_MODIFIED_LOCALLY"; +export interface AddProjectDocumentsAction { + type: typeof ADD_PROJECT_DOCUMENTS; + projectUid: string; + documents: Record; +} + +export interface ActivateProjectAction { + type: typeof ACTIVATE_PROJECT; + projectUid: string; +} + +export interface DocumentResetAction { + type: typeof DOCUMENT_RESET; + documentUid: string; + projectUid: string; +} + +export interface DocumentUpdateModifiedLocallyAction { + type: typeof DOCUMENT_UPDATE_MODIFIED_LOCALLY; + documentUid: string; + projectUid: string; + isModified: boolean; +} + +export interface DocumentInitializeAction { + type: typeof DOCUMENT_INITIALIZE; + documentUid: string; + projectUid: string; + filename: string; +} + +export interface DocumentRenameLocallyAction { + type: typeof DOCUMENT_RENAME_LOCALLY; + documentUid: string; + projectUid: string; + newFilename: string; +} + +export interface DocumentRemoveLocallyAction { + type: typeof DOCUMENT_REMOVE_LOCALLY; + documentUid: string; + projectUid: string; +} + +export interface DocumentSaveAction { + type: typeof DOCUMENT_SAVE; + document: IDocument; + projectUid: string; +} + +export interface DocumentUpdateValueAction { + type: typeof DOCUMENT_UPDATE_VALUE; + documentUid: string; + projectUid: string; + val: string; +} + +export interface CloseProjectAction { + type: typeof CLOSE_PROJECT; +} + +export interface StoreProjectLocallyAction { + type: typeof STORE_PROJECT_LOCALLY; + projects: IProject[]; +} + +export interface StoreProjectStarsAction { + type: typeof STORE_PROJECT_STARS; + projectUid: string; + stars: Star; +} + +export interface SetProjectPublicAction { + type: typeof SET_PROJECT_PUBLIC; + projectUid: string; + isPublic: boolean; +} +export interface UnsetProjectAction { + projectUid: string; + type: typeof UNSET_PROJECT; +} + +export interface UpdateProjectLastModifiedLocallyAction { + type: typeof UPDATE_PROJECT_LAST_MODIFIED_LOCALLY; + projectUid: string; + lastModified: number; + timestamp: number; +} + +export type ProjectsActionTypes = + | ActivateProjectAction + | AddProjectDocumentsAction + | DocumentResetAction + | DocumentUpdateModifiedLocallyAction + | DocumentInitializeAction + | DocumentRemoveLocallyAction + | DocumentSaveAction + | DocumentUpdateValueAction + | StoreProjectStarsAction + | CloseProjectAction + | UnsetProjectAction + | StoreProjectLocallyAction + | SetProjectPublicAction + | UpdateProjectLastModifiedLocallyAction; + export type IDocumentFileType = "txt" | "bin" | "folder"; // INTERFACES export interface IDocument { currentValue: string; - created: Timestamp | null; + created: number | undefined; documentUid: string; filename: string; - lastModified: Timestamp | null; + lastModified: number | undefined; savedValue: string; type: IDocumentFileType; userUid: string; @@ -35,23 +142,24 @@ export interface IDocument { path: string[]; } -export type IDocumentsMap = { [documentUid: string]: IDocument }; +// export type IDocumentsMap = { [documentUid: string]: IDocument }; -type IStar = { [userUid: string]: Timestamp }; +export type Star = { [userUid: string]: number }; export interface IProject { + created?: Timestamp; description: string; userUid: string; projectUid: string; name: string; isPublic: boolean; - documents: IDocumentsMap; - cachedProjectLastModified?: Timestamp; + documents: Record; + cachedProjectLastModified?: number; iconBackgroundColor?: string; iconForegroundColor?: string; iconName?: string; // only local path, NOT stored there on firestore - stars: IStar; + stars: Star; tags: string[]; } @@ -59,3 +167,10 @@ export interface IProjectsReducer { activeProjectUid?: string; projects: { [projectUid: string]: IProject }; } + +export interface CsoundFile { + filename: string; + path: string[]; + lastModified: number | undefined; + type: IDocumentFileType; +} diff --git a/src/components/projects/utils.tsx b/src/components/projects/utils.tsx index fdaf561d..deaaceec 100644 --- a/src/components/projects/utils.tsx +++ b/src/components/projects/utils.tsx @@ -1,25 +1,19 @@ -import { doc, getDoc, QueryDocumentSnapshot } from "firebase/firestore"; -import { getDownloadURL } from "firebase/storage"; -import { getType as mimeLookup } from "mime"; import { - storageReference, - getFirebaseTimestamp, - projectLastModified -} from "@config/firestore"; + doc, + DocumentChange, + getDoc, + QueryDocumentSnapshot, + Timestamp +} from "firebase/firestore"; +import { getDownloadURL } from "firebase/storage"; +import { Mime } from "mime"; +import { storageReference, projectLastModified } from "@config/firestore"; import { IFirestoreDocument, IFirestoreProject } from "@db/types"; -import { IDocument, IDocumentsMap, IDocumentFileType, IProject } from "./types"; -import { CsoundObj } from "@csound/browser"; -import { - assoc, - dropLast, - isNil, - map, - pipe, - prop, - propOr, - reduce, - reject -} from "ramda"; +import { IDocument, IDocumentFileType, IProject } from "./types"; +import { CsoundObj } from "@comp/csound/types"; +import { dropLast, isNil, prop, propOr, reject } from "ramda"; + +const mime = new Mime(); export function textOrBinary(filename: string): IDocumentFileType { const textFiles = [".csd", ".sco", ".orc", ".udo", ".txt", ".md", ".inc"]; @@ -33,7 +27,7 @@ export function textOrBinary(filename: string): IDocumentFileType { export function isAudioFile(fileName: string): boolean { // currently does not deal with FLAC, not sure if browser supports it - const mimeType = mimeLookup(fileName) || ""; + const mimeType = mime.getType(fileName) || ""; const endings = [".wav", ".ogg", ".mp3", "aiff", "flac"]; const lower = fileName.toLowerCase(); return ( @@ -52,9 +46,9 @@ export const generateEmptyDocument = ( ): IDocument => ({ filename, currentValue: "", - created: getFirebaseTimestamp(), + created: Date.now(), documentUid, - lastModified: getFirebaseTimestamp(), + lastModified: Date.now(), savedValue: "", type: "txt", userUid: "", @@ -62,7 +56,7 @@ export const generateEmptyDocument = ( path: [] }); -export const addDocumentToEMFS = async ( +export const addDocumentToCsoundFS = async ( projectUid: string, csound: CsoundObj, document: IDocument, @@ -108,34 +102,51 @@ export const addDocumentToEMFS = async ( }; export const fileDocumentDataToDocumentType = ( - documentData: IFirestoreDocument + documentData: IFirestoreDocument, + documentUid: string ): IDocument => ({ - created: documentData["created"], + created: documentData?.created?.toMillis() ?? undefined, currentValue: documentData["value"], description: documentData["description"], - documentUid: documentData["documentUid"], + documentUid, filename: documentData["name"], isModifiedLocally: false, - lastModified: documentData["lastModified"], + lastModified: documentData?.lastModified?.toMillis() ?? undefined, savedValue: documentData["value"], type: documentData["type"], userUid: documentData["userUid"], path: reject(isNil, documentData["path"] || []) - } as IDocument); + }) as IDocument; export const convertDocumentSnapToDocumentsMap = ( - documentsToAdd: IFirestoreDocument[] -): Record => - (pipe as any)( - map(prop("doc")), - map((d: any) => assoc("documentUid", d.id, d.data())), - reduce((accumulator: IDocumentsMap, documentData: any) => { - accumulator[documentData["documentUid"]] = - fileDocumentDataToDocumentType(documentData); - return accumulator; - }, {}) - )(documentsToAdd); + documentsToAdd: DocumentChange[] +): Record => { + return documentsToAdd + .map((doc) => [doc.doc, doc.doc.data()]) + .reduce( + ( + accumulator: Record, + [doc, documentData]: any[] + ) => { + const documentUid = doc.id; + accumulator[doc.id] = fileDocumentDataToDocumentType( + documentData, + documentUid + ); + // console.log( + // "Document data", + // documentUid, + // doc, + // documentData, + // accumulator[doc.id] + // ); + + return accumulator; + }, + {} + ); +}; export const firestoreProjectToIProject = ( project: IFirestoreProject @@ -159,14 +170,19 @@ export const convertProjectSnapToProject = async ( const projData = projSnap.data(); const lastModified = await getDoc(doc(projectLastModified, projSnap.id)); const lastModifiedData = lastModified.exists() && lastModified.data(); + const project = firestoreProjectToIProject(projData as IFirestoreProject); project["projectUid"] = projSnap.id; if (lastModifiedData && lastModifiedData.target) { - project["cachedProjectLastModified"] = lastModifiedData.target; + project["cachedProjectLastModified"] = + lastModifiedData.target.toMillis(); } if (projData.created) { - project["created"] = projData.created; + project.created = Timestamp.fromMillis( + (projData.created as Timestamp).toMillis() + ); } + return project; }; diff --git a/src/components/router/router.tsx b/src/components/router/router.tsx index d1cf188f..09fb36b9 100644 --- a/src/components/router/router.tsx +++ b/src/components/router/router.tsx @@ -1,85 +1,19 @@ -import React, { useEffect } from "react"; -import { Provider as Provider_, useDispatch } from "react-redux"; -import ReactTooltip from "react-tooltip"; import Home from "../home/home"; import CsoundManual from "@comp/manual/manual"; -import Profile from "../profile/profile"; -import Page404 from "../page-404/page-404"; -import ProjectContext from "../projects/project-context"; -import { closeTabDock } from "@comp/project-editor/actions"; -import { history, store } from "@store"; -import { closeProject } from "@comp/projects/actions"; -import { HistoryRouter as Router } from "redux-first-history/rr6"; -import { Route, Routes } from "react-router-dom"; -import { stopCsound } from "../csound/actions"; -import SiteDocuments from "../site-documents/site-documents"; -import { ConsoleProvider } from "@comp/console/context"; -import { ParserDebugger } from "@comp/editor/modes/csound/debug-parser"; - -const Provider = Provider_ as any; - -const EditorLayout = (properties: any) => { - const dispatch = useDispatch(); - - useEffect(() => { - return () => { - dispatch(stopCsound()); - dispatch(closeProject()); - dispatch(closeTabDock()); - }; - }, [dispatch]); - - return ( - - - - - - ); -}; - -// const CsoundManualWithStyleOverrides = ({ -// theme, -// ...routerProperties -// }: any) => { -// const [isMounted, setIsMounted] = useState(false); -// const [{ fetched, Csound }, setFetchState]: [ -// { fetched: boolean; Csound: any }, -// any -// ] = useState({ fetched: false, Csound: undefined }); - -// useEffect(() => { -// if (!isMounted) { -// setIsMounted(true); -// import("@csound/browser").then(({ Csound }) => { -// setFetchState({ fetched: true, Csound }); -// }); -// } -// }, [isMounted, setIsMounted, fetched, Csound, setFetchState]); - -// return !fetched ? ( -// <> -// ) : ( -// -// ); -// }; - -const RouterComponent = (): React.ReactElement => { - ReactTooltip.rebuild(); +import { Profile } from "../profile/profile"; +import { Page404 } from "../page-404/page-404"; +import { ProjectContext } from "../projects/project-context"; +import { BrowserRouter, Route, Routes } from "react-router"; +import { SiteDocuments } from "../site-documents/site-documents"; +export const WebIdeRouter = () => { return ( - + } /> - } /> - } /> - }> - } /> + } /> + }> + } /> }> } /> @@ -87,11 +21,8 @@ const RouterComponent = (): React.ReactElement => { } /> } /> - } /> } /> - + ); }; - -export default RouterComponent; diff --git a/src/components/router/selectors.tsx b/src/components/router/selectors.tsx deleted file mode 100644 index b95be262..00000000 --- a/src/components/router/selectors.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { IStore } from "@store/types"; -import { notEmpty } from "@root/utils"; -import { always, cond, equals, match, T } from "ramda"; - -export const selectCurrentRoute = ({ router }: IStore): string => { - return cond([ - [equals("/"), always("home")], - [(x) => notEmpty(match(/^\/editor\/+/g, x)), always("editor")], - [(x) => notEmpty(match(/^\/profile/g, x)), always("profile")], - [T, always("404")] - ])(router.location.pathname); -}; - -export const selectCurrentProfileRoute = ({ - router -}: IStore): Array => { - if (notEmpty(match(/^\/profile\//g, router.location.pathname))) { - const woPrefix = router.location.pathname.replace(/^\/profile\//g, ""); - const woPostfix = woPrefix.replace(/\/.*/g, ""); - if (notEmpty(match(/^\/profile\/.*\/.*/g, router.location.pathname))) { - const nestedPath = woPrefix.split("/"); - return [woPostfix, nestedPath[1]]; - } else { - return [woPostfix, undefined]; - } - } else { - return [undefined, undefined]; - } -}; diff --git a/src/components/share-dialog/index.tsx b/src/components/share-dialog/index.tsx index 56d3843b..629a8dae 100644 --- a/src/components/share-dialog/index.tsx +++ b/src/components/share-dialog/index.tsx @@ -48,10 +48,7 @@ const ShareDialog = (): React.ReactElement => {

Share

- + Promise) => { return async (dispatch: any) => { - const shortcuts = () => ( -
-

Editor Actions

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ShortcutAction
ctrl-eEvaluate current line
ctrl-enterEvaluate current block
ctrl-. - Show opcode documentation for opcode at cursor -
ctrl-;Toggle comment
 
ctrl-sSave current document
shift-ctrl-sSave all documents
 
ctrl-rRun/Restart realtime rendering
ctrl-pPause realtime rendering
-
- ); - dispatch(openSimpleModal(shortcuts, {})); + dispatch(openSimpleModal("keyboard-shortcuts", {})); }; }; diff --git a/src/components/site-documents/keyboard-shortcuts.tsx b/src/components/site-documents/keyboard-shortcuts.tsx new file mode 100644 index 00000000..9ca7a17d --- /dev/null +++ b/src/components/site-documents/keyboard-shortcuts.tsx @@ -0,0 +1,57 @@ +import React from "react"; + +export const KeyboardShortcuts = () => ( +
+

Editor Actions

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ShortcutAction
ctrl-eEvaluate current line
ctrl-enterEvaluate current block
ctrl-.Show opcode documentation for opcode at cursor
ctrl-;Toggle comment
 
ctrl-sSave current document
shift-ctrl-sSave all documents
 
ctrl-rRun/Restart realtime rendering
ctrl-pPause realtime rendering
+
+); diff --git a/src/components/site-documents/site-documents.tsx b/src/components/site-documents/site-documents.tsx index 07f3d45a..0251c44b 100644 --- a/src/components/site-documents/site-documents.tsx +++ b/src/components/site-documents/site-documents.tsx @@ -1,243 +1,230 @@ -import React, { Component } from "react"; -import withStyles from "./styles"; -import Header from "../header/header"; - -class SiteDocuments extends Component { - public render() { - const { classes } = this.props; - return ( -
-
-
-
-

Csound Web-IDE

- -

Welcome to the Csound Web-IDE!

-

- This site is an online Integrated Development - Environment (IDE) for{" "} - Csound, a sound and - music computing system that has its roots in the - earliest of computer software, the MUSIC-N series by{" "} - - Max Mathews - - . Csound was originally released in 1986 by{" "} - - Barry Vercoe - {" "} - and it has been a part of the computer music world - since. Today's Csound works on desktop, mobile, - embedded, server, and web platforms and powers music - software and music-making for musician's around - the world. -

- -

- With this site we bring you an online, social coding - Csound development experience with the same features - found traditionally on desktop systems. We hope the - features to compose, live code, share, and discover - Csound work will help power your musical endeavors. -

-
- -
-

Getting Started

- -

Searching and Browsing Projects

- -

- The home page provides a search option for finding - public projects. Enter in search terms to find - projects matching the search terms. You can click on - the project to view and run the project as well as - click on the author to look at their profile. On the - author's profile you will find a list of public - projects they have shared as well as have the - opportunity to follow that user (if you are logged - in). -

- -

Sign up for an account

- -

- To use the site for your own work, sign up for an - account by using the "Login" button in the - top bar. You can sign up using an email account or - sign-in using a Google or Facebook account. Once you - sign in, you will need to choose a unique username - for your account. -

- -

View/Edit Profile

- -

- The profile page houses your projects. It will show - all of your project, public or private, as well as - the list of people you follow. -

-

- You can edit your information on the profile page as - well, including updating your user image, bio, and - links. -

-

- The profile page is also where you will be creating - and modifying project details. You can also audition - projects using the play button on the project as - well as select the project to view/edit the project - in the main IDE editor. -

- -

Need assistance?

- -

- The easiest way to get help with the Web-IDE is to - join the{" "} - - Csound Slack - {" "} - and ask questions in the #web-ide channel. -

- -

Have an issue or find a bug?

- -

- If you have an issue, find a bug, or want to make a - suggestion for improvement, please either file an - issue on the Web-IDE's{" "} - - Github issue tracker - - . The tracker allows us to follow up with issues - when we can and have a conversation to figure out - the best way to address the issue. We look forward - to hearing from you! -

-
- -
-

Creating a New Project

- -

- On your profile page, use the "Create +" - button to create a new project. A modal dialog will - appear where you can fill in the name of the - project, description, and any tags. You can later - edit the project information using the - "Edit" button. The name, description and - tags will be visible for the project on your profile - page for you and others who may look at your profile - (if your project is public). These fields will also - be used when users search for projects. -

- -

Editing a Project

- - Project Editor - -

- The project editor is made up of a number of - components: -

- -
    -
  • Menu Bar
  • -
  • Project File Tree
  • -
  • File Editors
  • -
  • Console Output
  • -
  • Play Controls
  • -
  • Social Controls
  • -
  • - Csound Manual Panel (not shown in screenshot - above) -
  • -
- -

Menu Bar

- -

- The menu bar contains dropdown menus for various - operations in the Web-IDE such as saving files, - opening and closing views (i.e., Project File Tree, - Console Ouptput, Csound Manual Panel), and more. -

- -

Project File Tree

- -

- Projects are made up of files laid out in a - directory structure much like they would be when - working with Csound on a desktop operating system. - Click on a file in the file tree to open up its - editor. Organize your code and resource files and - use relative paths in the same way as you would if - using Csound on the desktop. -

- -

File Editors

- -

- Editors appear for editing code and working with - resource files. Code editors provide syntax - highlighting, shortcuts for code evaluation (i.e., - live coding), and code completion. -

- -

Console Output

- -

- The Console Output shows the output messages - generated when Csound runs.{" "} -

- -

Play Controls

- -

- The Play controls include a Play/Pause button, Stop - button, and target selection. Each Web-IDE supports - using different CSDs as Run targets. This allows you - to create a project with multiple CSDs that share - code. Use the "Configure" command in the - target dropdown to manage targets for your project. -

- -

Social Controls

- -

- The social controls provide a button to share the - project via Facebook, Twitter, and Email; a Star - button to like the project; and an eye button to - mark the project as public or private. If you are - not the author of the project the eye button will - not be available, but you may still share or like - the project. -

- -

Csound Manual Panel

- -

- The Web-IDE provides a built-in version of the - Csound Manual that you can search through. Examples - found in manual entries can also be auditioned - directly in the manual panel. -

-
- -
-

Audio/MIDI Input

- -

-
-
-
- ); - } -} - -export default withStyles(SiteDocuments); +import { mainStyle, rootStyle } from "./styles"; +import { Header } from "../header/header"; +import { main } from "../projects/styles"; + +export const SiteDocuments = () => { + return ( +
+
+
+
+

Csound Web-IDE

+ +

Welcome to the Csound Web-IDE!

+

+ This site is an online Integrated Development + Environment (IDE) for{" "} + Csound, a sound and + music computing system that has its roots in the + earliest of computer software, the MUSIC-N series by{" "} + + Max Mathews + + . Csound was originally released in 1986 by{" "} + Barry Vercoe{" "} + and it has been a part of the computer music world + since. Today's Csound works on desktop, mobile, + embedded, server, and web platforms and powers music + software and music-making for musician's around the + world. +

+ +

+ With this site we bring you an online, social coding + Csound development experience with the same features + found traditionally on desktop systems. We hope the + features to compose, live code, share, and discover + Csound work will help power your musical endeavors. +

+
+ +
+

Getting Started

+ +

Searching and Browsing Projects

+ +

+ The home page provides a search option for finding + public projects. Enter in search terms to find projects + matching the search terms. You can click on the project + to view and run the project as well as click on the + author to look at their profile. On the author's + profile you will find a list of public projects they + have shared as well as have the opportunity to follow + that user (if you are logged in). +

+ +

Sign up for an account

+ +

+ To use the site for your own work, sign up for an + account by using the "Login" button in the top + bar. You can sign up using an email account or sign-in + using a Google or Facebook account. Once you sign in, + you will need to choose a unique username for your + account. +

+ +

View/Edit Profile

+ +

+ The profile page houses your projects. It will show all + of your project, public or private, as well as the list + of people you follow. +

+

+ You can edit your information on the profile page as + well, including updating your user image, bio, and + links. +

+

+ The profile page is also where you will be creating and + modifying project details. You can also audition + projects using the play button on the project as well as + select the project to view/edit the project in the main + IDE editor. +

+ +

Need assistance?

+ +

+ The easiest way to get help with the Web-IDE is to join + the{" "} + + Csound Slack + {" "} + and ask questions in the #web-ide channel. +

+ +

Have an issue or find a bug?

+ +

+ If you have an issue, find a bug, or want to make a + suggestion for improvement, please either file an issue + on the Web-IDE's{" "} + + Github issue tracker + + . The tracker allows us to follow up with issues when we + can and have a conversation to figure out the best way + to address the issue. We look forward to hearing from + you! +

+
+ +
+

Creating a New Project

+ +

+ On your profile page, use the "Create +" + button to create a new project. A modal dialog will + appear where you can fill in the name of the project, + description, and any tags. You can later edit the + project information using the "Edit" button. + The name, description and tags will be visible for the + project on your profile page for you and others who may + look at your profile (if your project is public). These + fields will also be used when users search for projects. +

+ +

Editing a Project

+ + Project Editor + +

+ The project editor is made up of a number of components: +

+ +
    +
  • Menu Bar
  • +
  • Project File Tree
  • +
  • File Editors
  • +
  • Console Output
  • +
  • Play Controls
  • +
  • Social Controls
  • +
  • + Csound Manual Panel (not shown in screenshot above) +
  • +
+ +

Menu Bar

+ +

+ The menu bar contains dropdown menus for various + operations in the Web-IDE such as saving files, opening + and closing views (i.e., Project File Tree, Console + Ouptput, Csound Manual Panel), and more. +

+ +

Project File Tree

+ +

+ Projects are made up of files laid out in a directory + structure much like they would be when working with + Csound on a desktop operating system. Click on a file in + the file tree to open up its editor. Organize your code + and resource files and use relative paths in the same + way as you would if using Csound on the desktop. +

+ +

File Editors

+ +

+ Editors appear for editing code and working with + resource files. Code editors provide syntax + highlighting, shortcuts for code evaluation (i.e., live + coding), and code completion. +

+ +

Console Output

+ +

+ The Console Output shows the output messages generated + when Csound runs.{" "} +

+ +

Play Controls

+ +

+ The Play controls include a Play/Pause button, Stop + button, and target selection. Each Web-IDE supports + using different CSDs as Run targets. This allows you to + create a project with multiple CSDs that share code. Use + the "Configure" command in the target dropdown + to manage targets for your project. +

+ +

Social Controls

+ +

+ The social controls provide a button to share the + project via Facebook, Twitter, and Email; a Star button + to like the project; and an eye button to mark the + project as public or private. If you are not the author + of the project the eye button will not be available, but + you may still share or like the project. +

+ +

Csound Manual Panel

+ +

+ The Web-IDE provides a built-in version of the Csound + Manual that you can search through. Examples found in + manual entries can also be auditioned directly in the + manual panel. +

+
+ +
+

Audio/MIDI Input

+ +

+
+
+
+ ); +}; diff --git a/src/components/site-documents/styles.ts b/src/components/site-documents/styles.ts index fb00e22b..2a85463c 100644 --- a/src/components/site-documents/styles.ts +++ b/src/components/site-documents/styles.ts @@ -1,37 +1,27 @@ -import { Theme } from "@material-ui/core"; -import { createStyles, withStyles } from "@material-ui/styles"; +import { css } from "@emotion/react"; import { headerHeight } from "@styles/constants"; -// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types -const siteDocumentsStyles = (theme: Theme) => - createStyles({ - root: { - backgroundColor: "#e8e8e8", - bottom: "0px", - top: `${headerHeight}px`, - left: 0, - right: 0, - position: "relative" - }, - main: { - maxWidth: "1024px", - padding: 16, - margin: "auto", - fontSize: 16, - "& h1": { - margin: "16px 0" - }, - "& h2": { - margin: "40px 0 16px" - }, - "& h3": { - margin: "40px 0 16px" - } - } - }); +export const rootStyle = css({ + backgroundColor: "#e8e8e8", + bottom: "0px", + top: `${headerHeight}px`, + left: 0, + right: 0, + position: "relative" +}); -// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types -const withStyles_ = (ClassComponent: any) => - withStyles(siteDocumentsStyles)(ClassComponent); - -export default withStyles_; +export const mainStyle = css({ + maxWidth: "1024px", + padding: 16, + margin: "auto", + fontSize: 16, + "& h1": { + margin: "16px 0" + }, + "& h2": { + margin: "40px 0 16px" + }, + "& h3": { + margin: "40px 0 16px" + } +}); diff --git a/src/components/snackbar/actions.ts b/src/components/snackbar/actions.ts index 4bf5b71d..c152d385 100644 --- a/src/components/snackbar/actions.ts +++ b/src/components/snackbar/actions.ts @@ -2,7 +2,6 @@ import { OPEN_SNACKBAR, CLOSE_SNACKBAR, - SnackbarActionTypes, SnackbarType, ISnackbar } from "./types"; @@ -11,7 +10,7 @@ export const openSnackbar = ( text: string, type: SnackbarType, timeout = 6000 -): SnackbarActionTypes => { +) => { const payload: ISnackbar = { text, type, @@ -23,7 +22,7 @@ export const openSnackbar = ( }; }; -export const closeSnackbar = (): SnackbarActionTypes => { +export const closeSnackbar = () => { return { type: CLOSE_SNACKBAR }; diff --git a/src/components/snackbar/reducer.ts b/src/components/snackbar/reducer.ts index 54bc7b25..52a0ef10 100644 --- a/src/components/snackbar/reducer.ts +++ b/src/components/snackbar/reducer.ts @@ -1,6 +1,7 @@ import { OPEN_SNACKBAR, CLOSE_SNACKBAR, + OpenSnackbar, SnackbarType, SnackbarActionTypes } from "./types"; @@ -20,11 +21,16 @@ const INITIAL_STATE: ISnackbarReducer = { }; const SnackbarReducer = ( - state = INITIAL_STATE, - action: SnackbarActionTypes + state: ISnackbarReducer | undefined, + unknownAction: SnackbarActionTypes ): ISnackbarReducer => { - switch (action.type) { + if (!state) { + return INITIAL_STATE; + } + + switch (unknownAction.type) { case OPEN_SNACKBAR: { + const action = unknownAction as OpenSnackbar; return { ...state, ...action.payload, diff --git a/src/components/snackbar/selectors.ts b/src/components/snackbar/selectors.ts index 6e357cf9..814d17ac 100644 --- a/src/components/snackbar/selectors.ts +++ b/src/components/snackbar/selectors.ts @@ -1,24 +1,24 @@ // import { createSelector } from "reselect"; import { ISnackbarReducer } from "./reducer"; import { SnackbarType } from "./types"; -import { IStore } from "@store/types"; +import { RootState } from "@root/store"; -export const selectSnackbarOpen = (store: IStore): boolean => { +export const selectSnackbarOpen = (store: RootState): boolean => { const state: ISnackbarReducer = store.SnackbarReducer; return state.open; }; -export const selectSnackbarType = (store: IStore): SnackbarType => { +export const selectSnackbarType = (store: RootState): SnackbarType => { const state: ISnackbarReducer = store.SnackbarReducer; return state.type; }; -export const selectSnackbarText = (store: IStore): string => { +export const selectSnackbarText = (store: RootState): string => { const state: ISnackbarReducer = store.SnackbarReducer; return state.text; }; -export const selectSnackbarTimeout = (store: IStore): number => { +export const selectSnackbarTimeout = (store: RootState): number => { const state: ISnackbarReducer = store.SnackbarReducer; return state.timeout; }; diff --git a/src/components/snackbar/snackbar.tsx b/src/components/snackbar/snackbar.tsx index 9f0c3bc4..d33e88a1 100644 --- a/src/components/snackbar/snackbar.tsx +++ b/src/components/snackbar/snackbar.tsx @@ -1,15 +1,13 @@ import React from "react"; -import clsx from "clsx"; -import CheckCircleIcon from "@material-ui/icons/CheckCircle"; -import ErrorIcon from "@material-ui/icons/Error"; -import InfoIcon from "@material-ui/icons/Info"; -import CloseIcon from "@material-ui/icons/Close"; -import { amber, green } from "@material-ui/core/colors"; -import IconButton from "@material-ui/core/IconButton"; -import Snackbar from "@material-ui/core/Snackbar"; -import SnackbarContent from "@material-ui/core/SnackbarContent"; -import WarningIcon from "@material-ui/icons/Warning"; -import { makeStyles, Theme } from "@material-ui/core/styles"; +import CheckCircleIcon from "@mui/icons-material/CheckCircle"; +import ErrorIcon from "@mui/icons-material/Error"; +import InfoIcon from "@mui/icons-material/Info"; +import CloseIcon from "@mui/icons-material/Close"; +import IconButton from "@mui/material/IconButton"; +import Snackbar from "@mui/material/Snackbar"; +import SnackbarContent from "@mui/material/SnackbarContent"; +import WarningIcon from "@mui/icons-material/Warning"; +import { Theme, useTheme } from "@emotion/react"; import { closeSnackbar } from "./actions"; import { useSelector, useDispatch } from "react-redux"; import { @@ -27,36 +25,36 @@ const variantIcon = { info: InfoIcon }; -const styles = makeStyles((theme: Theme) => ({ - success: { - backgroundColor: green[600], - color: theme.palette.common.white - }, - error: { - backgroundColor: theme.palette.error.dark, - color: theme.palette.common.white - }, - info: { - backgroundColor: theme.palette.primary.main, - color: theme.palette.common.white - }, - warning: { - backgroundColor: amber[700], - color: theme.palette.common.white - }, - icon: { - fontSize: 20 - }, - iconVariant: { - opacity: 0.9, - marginRight: theme.spacing(1) - }, - message: { - display: "flex", - alignItems: "center", - color: theme.palette.common.black - } -})); +// const styles = (theme: Theme) => ({ +// success: { +// backgroundColor: green[600], +// color: theme.palette.common.white +// }, +// error: { +// backgroundColor: theme.palette.error.dark, +// color: theme.palette.common.white +// }, +// info: { +// backgroundColor: theme.palette.primary.main, +// color: theme.palette.common.white +// }, +// warning: { +// backgroundColor: amber[700], +// color: theme.palette.common.white +// }, +// icon: { +// fontSize: 20 +// }, +// iconVariant: { +// opacity: 0.9, +// marginRight: theme.spacing(1) +// }, +// message: { +// display: "flex", +// alignItems: "center", +// color: theme.palette.common.black +// } +// }); export interface IProperties { className?: string; @@ -65,62 +63,6 @@ export interface IProperties { variant: SnackbarType; } -const SnackbarContentWrapper = (properties: IProperties) => { - const classes = styles(); - const { className, message, onClose, variant, ...rest } = properties; - - let Icon; - let cssClass; - - switch (variant) { - case SnackbarType.Info: - Icon = variantIcon["info"]; - cssClass = classes["info"]; - break; - case SnackbarType.Warning: - Icon = variantIcon["warning"]; - cssClass = classes["warning"]; - break; - case SnackbarType.Success: - Icon = variantIcon["success"]; - cssClass = classes["success"]; - break; - case SnackbarType.Error: - Icon = variantIcon["error"]; - cssClass = classes["error"]; - break; - default: - Icon = variantIcon["error"]; - cssClass = classes["error"]; - - break; - } - - return ( - - - {message} - - } - action={[ - - - - ]} - {...rest} - /> - ); -}; - const CustomSnackbar = (): React.ReactElement => { const type = useSelector(selectSnackbarType); const text = useSelector(selectSnackbarText); @@ -140,14 +82,9 @@ const CustomSnackbar = (): React.ReactElement => { }} open={open} autoHideDuration={timeout} + message={text} onClose={handleClose} - > - - + /> ); }; diff --git a/src/components/snackbar/types.ts b/src/components/snackbar/types.ts index fbaaf180..3e775dd3 100644 --- a/src/components/snackbar/types.ts +++ b/src/components/snackbar/types.ts @@ -1,3 +1,5 @@ +import { UnknownAction } from "redux"; + export const OPEN_SNACKBAR = "SNACKBAR.OPEN_SNACKBAR"; export const CLOSE_SNACKBAR = "SNACKBAR.CLOSE_SNACKBAR"; @@ -6,12 +8,12 @@ export interface ISnackbar { text: string; timeout: number | typeof Number.POSITIVE_INFINITY; } -interface OpenSnackbar { +export interface OpenSnackbar { type: typeof OPEN_SNACKBAR; payload: ISnackbar; } -interface CloseSnackbar { +export interface CloseSnackbar { type: typeof CLOSE_SNACKBAR; } @@ -22,4 +24,4 @@ export enum SnackbarType { Success } -export type SnackbarActionTypes = OpenSnackbar | CloseSnackbar; +export type SnackbarActionTypes = OpenSnackbar | CloseSnackbar | UnknownAction; diff --git a/src/components/social-controls/selectors.ts b/src/components/social-controls/selectors.ts index 09406216..2cb94783 100644 --- a/src/components/social-controls/selectors.ts +++ b/src/components/social-controls/selectors.ts @@ -1,32 +1,45 @@ -import { IStore } from "@store/types"; -import { IProject, IProjectsReducer } from "../projects/types"; -import { curry, keys, pathOr, propOr } from "ramda"; +import { RootState } from "@root/store"; +import { createSelector } from "@reduxjs/toolkit"; +import { IProject, IProjectsReducer, Star } from "../projects/types"; +import { keys, propOr } from "ramda"; import { selectActiveProject } from "../projects/selectors"; -export const selectActiveProjectUid = (store: IStore): string | undefined => { +export const selectActiveProjectUid = ( + store: RootState +): string | undefined => { const state: IProjectsReducer = store.ProjectsReducer; return state.activeProjectUid; }; export const selectProjects = ( - store: IStore + store: RootState ): { [projectUid: string]: IProject } | undefined => { const state: IProjectsReducer = store.ProjectsReducer; return state.projects; }; -export const selectUserStarredProject = curry( - (loggedInUserUid: string, projectUid: string, store: IStore) => { - const projectStars: string[] = pathOr( - [], - ["ProjectsReducer", "projects", projectUid, "stars"], - store - ); - return keys(projectStars).includes(loggedInUserUid); - } -); +export const selectProjectStars = (projectUid: string) => + createSelector( + [ + (state: RootState) => + state.ProjectsReducer.projects[projectUid]?.stars + ], + (projectStars: Star | undefined) => projectStars || {} + ); -export const selectProjectPublic = (store: IStore): boolean => { +export const selectUserStarredProject = ( + loggedInUserUid: string | undefined, + projectUid: string | undefined +) => + createSelector( + [selectProjectStars(projectUid || "")], + (projectStars: Star) => { + if (!projectUid || !loggedInUserUid) return false; + return Object.keys(projectStars).includes(loggedInUserUid); + } + ); + +export const selectProjectPublic = (store: RootState): boolean => { const activeProject = selectActiveProject(store); return propOr(true, "isPublic", activeProject); }; diff --git a/src/components/social-controls/social-controls.tsx b/src/components/social-controls/social-controls.tsx index b5058633..32cf31fb 100644 --- a/src/components/social-controls/social-controls.tsx +++ b/src/components/social-controls/social-controls.tsx @@ -1,17 +1,17 @@ import React, { useEffect } from "react"; +import { useDispatch, useSelector } from "@root/store"; import * as SS from "./styles"; import { subscribeToProjectStars } from "./subscribers"; -import Tooltip from "@material-ui/core/Tooltip"; -import { useSelector, useDispatch } from "react-redux"; -import { IconButton } from "@material-ui/core"; +import Tooltip from "@mui/material/Tooltip"; +import { IconButton } from "@mui/material"; import { exportProject, markProjectPublic } from "@comp/projects/actions"; -import StarIcon from "@material-ui/icons/Star"; -import OutlinedStarIcon from "@material-ui/icons/StarBorderOutlined"; -import CloudDownloadIcon from "@material-ui/icons/CloudDownload"; -import VisibilityIcon from "@material-ui/icons/Visibility"; -import VisibilityOffIcon from "@material-ui/icons/VisibilityOff"; -import ShareIcon from "@material-ui/icons/Share"; -import styled from "styled-components"; +import StarIcon from "@mui/icons-material/Star"; +import OutlinedStarIcon from "@mui/icons-material/StarBorderOutlined"; +import CloudDownloadIcon from "@mui/icons-material/CloudDownload"; +import VisibilityIcon from "@mui/icons-material/Visibility"; +import VisibilityOffIcon from "@mui/icons-material/VisibilityOff"; +import ShareIcon from "@mui/icons-material/Share"; +import styled from "@emotion/styled"; import { selectUserStarredProject, selectActiveProjectUid, @@ -25,7 +25,6 @@ import { import { starOrUnstarProject } from "@comp/profile/actions"; import { selectIsOwner } from "@comp/project-editor/selectors"; import { openSimpleModal } from "../modal/actions"; -import ShareDialog from "../share-dialog"; const StyledIconButton = styled(IconButton)` && { @@ -35,7 +34,7 @@ const StyledIconButton = styled(IconButton)` `; const StyledDownloadIcon = styled(CloudDownloadIcon)` && { - fill: ${(properties) => properties.theme.socialIcon}; + fill: ${(properties) => properties.theme.altTextColor}; } `; const StyledStarIcon = styled(StarIcon)` @@ -46,45 +45,47 @@ const StyledStarIcon = styled(StarIcon)` const StyledOutlinedStarIcon = styled(OutlinedStarIcon)` && { - fill: ${(properties) => properties.theme.starInactive}; + fill: ${(properties) => properties.theme.altTextColor}; } `; const StyledShareIcon = styled(ShareIcon)` && { - fill: ${(properties) => properties.theme.socialIcon}; + fill: ${(properties) => properties.theme.altTextColor}; } `; const StyledPublicIcon = styled(VisibilityIcon)` && { - fill: ${(properties) => properties.theme.publicIcon}; + fill: ${(properties) => properties.theme.altTextColor}; } `; const StyledPublicOffIcon = styled(VisibilityOffIcon)` && { - fill: ${(properties) => properties.theme.publicIcon}; + fill: ${(properties) => properties.theme.altTextColor}; } `; -const SocialControls = (): React.ReactElement => { - const projectUid = useSelector(selectActiveProjectUid); +const SocialControls = ({ activeProjectUid }: { activeProjectUid: string }) => { const loggedInUserUid = useSelector(selectLoggedInUid); const starred = useSelector( - selectUserStarredProject(loggedInUserUid, projectUid) + selectUserStarredProject(loggedInUserUid, activeProjectUid) ); const isRequestingLogin = useSelector(selectLoginRequesting); - const isOwner = useSelector(selectIsOwner(projectUid as any)); + const isOwner = useSelector(selectIsOwner); const isPublic = useSelector(selectProjectPublic); const dispatch = useDispatch(); useEffect(() => { - if (projectUid && !isRequestingLogin && loggedInUserUid) { - const unsubscribe = subscribeToProjectStars(projectUid, dispatch); + if (activeProjectUid && !isRequestingLogin && loggedInUserUid) { + const unsubscribe = subscribeToProjectStars( + activeProjectUid, + dispatch + ); return unsubscribe; } - }, [projectUid, isRequestingLogin, loggedInUserUid, dispatch]); + }, [activeProjectUid, isRequestingLogin, loggedInUserUid, dispatch]); return ( <> @@ -108,7 +109,7 @@ const SocialControls = (): React.ReactElement => { { - dispatch(openSimpleModal(ShareDialog, {})); + dispatch(openSimpleModal("share-dialog", {})); }} > @@ -124,10 +125,10 @@ const SocialControls = (): React.ReactElement => { { - if (projectUid && loggedInUserUid) { + if (activeProjectUid && loggedInUserUid) { dispatch( starOrUnstarProject( - projectUid, + activeProjectUid, loggedInUserUid ) ); @@ -148,9 +149,12 @@ const SocialControls = (): React.ReactElement => { { - if (typeof projectUid === "string") { + if (typeof activeProjectUid === "string") { dispatch( - markProjectPublic(projectUid, !isPublic) + markProjectPublic( + activeProjectUid, + !isPublic + ) ); } }} diff --git a/src/components/social-controls/subscribers.tsx b/src/components/social-controls/subscribers.tsx index bd38e80f..852f645b 100644 --- a/src/components/social-controls/subscribers.tsx +++ b/src/components/social-controls/subscribers.tsx @@ -1,18 +1,26 @@ import { STORE_PROJECT_STARS } from "@comp/projects/types"; import { stars } from "@config/firestore"; -import { doc, onSnapshot } from "firebase/firestore"; +import { Timestamp, doc, onSnapshot } from "firebase/firestore"; export const subscribeToProjectStars = ( projectUid: string, - dispatch: (any) => void + dispatch: (store: any) => void ): (() => void) => { const unsubscribe: () => void = onSnapshot( doc(stars, projectUid), (stars) => { + const starsData = stars.data(); + const starsDataSerializeable: Record = {}; + for (const sdk in starsData) { + const sdkd: Timestamp = starsData[sdk]; + if (typeof sdkd === "object") { + starsDataSerializeable[sdk] = sdkd.toMillis(); + } + } dispatch({ type: STORE_PROJECT_STARS, projectUid, - stars: stars.data() + stars: starsDataSerializeable }); }, (error: any) => console.error(error) diff --git a/src/components/spectral-analyzer/spectral-analyzer.tsx b/src/components/spectral-analyzer/spectral-analyzer.tsx index 35b19524..b44ddd3a 100644 --- a/src/components/spectral-analyzer/spectral-analyzer.tsx +++ b/src/components/spectral-analyzer/spectral-analyzer.tsx @@ -1,16 +1,13 @@ import React, { useEffect, useRef, useState } from "react"; import { useSelector } from "react-redux"; import { assoc, path } from "ramda"; -import { CsoundObj } from "@csound/browser"; +import { CsoundObj } from "@comp/csound/types"; import { ICsoundStatus } from "@comp/csound/types"; +import { csoundInstance } from "../csound"; import { scaleLinear } from "d3-scale"; -type ISpectralAnalyzerProperties = { - classes: any; -}; - // resize code used from https://webglfundamentals.org/webgl/lessons/webgl-resizing-the-canvas.html -function resize(canvas) { +function resize(canvas: HTMLCanvasElement) { // Lookup the size the browser is displaying the canvas. const displayWidth = canvas.clientWidth; const displayHeight = canvas.clientHeight; @@ -105,9 +102,7 @@ const connectVisualizer = async ( } }; -const SpectralAnalyzer = ({ - classes -}: ISpectralAnalyzerProperties): React.ReactElement => { +const SpectralAnalyzer = (): React.ReactElement => { const [scopeNodeState, setScopeNodeState]: [ { status: "init" | "running"; @@ -119,20 +114,17 @@ const SpectralAnalyzer = ({ scopeNodeDisconnector: undefined }); - const canvasReference = useRef() as CanvasReference; - - const csound: CsoundObj | undefined = useSelector( - path(["csound", "csound"]) - ); + const canvasReference = useRef(null) as CanvasReference; - const csoundStatus: ICsoundStatus = useSelector(path(["csound", "status"])); + const csoundStatus: ICsoundStatus = + useSelector(path(["csound", "status"])) || "initialized"; useEffect(() => { if ( ["stopped", "error"].includes(csoundStatus) && scopeNodeState.status === "running" ) { - if (csound && scopeNodeState.scopeNodeDisconnector) { + if (csoundInstance && scopeNodeState.scopeNodeDisconnector) { scopeNodeState.scopeNodeDisconnector(); } setScopeNodeState({ @@ -141,19 +133,20 @@ const SpectralAnalyzer = ({ }); } if ( - csound && + csoundInstance && csoundStatus === "playing" && scopeNodeState.status !== "running" ) { setScopeNodeState(assoc("status", "running", scopeNodeState)); - connectVisualizer( - csound, - canvasReference - ).then((scopeNodeDisconnector) => - setScopeNodeState({ status: "running", scopeNodeDisconnector }) + connectVisualizer(csoundInstance, canvasReference).then( + (scopeNodeDisconnector) => + setScopeNodeState({ + status: "running", + scopeNodeDisconnector + }) ); } - }, [csound, csoundStatus, scopeNodeState]); + }, [csoundStatus, scopeNodeState]); useEffect(() => { return () => { diff --git a/src/components/target-controls/actions.tsx b/src/components/target-controls/actions.tsx index 6d69f0f1..468f052c 100644 --- a/src/components/target-controls/actions.tsx +++ b/src/components/target-controls/actions.tsx @@ -4,7 +4,6 @@ import { openSimpleModal } from "@comp/modal/actions"; import { openSnackbar } from "@comp/snackbar/actions"; import { SnackbarType } from "@comp/snackbar/types"; import { database, targets as targetsCollReference } from "@config/firestore"; -import TargetsConfigDialog from "./targets-config-dialog"; import { ITargetMap, SET_SELECTED_TARGET, @@ -28,12 +27,12 @@ export const showTargetsConfigDialog = (): (( dispatch: any ) => Promise) => { return async (dispatch: any) => { - dispatch(openSimpleModal(TargetsConfigDialog, {})); + dispatch(openSimpleModal("target-controls", {})); }; }; export const updateAllTargetsLocally = ( - dispatch: (any) => void, + dispatch: (store: any) => void, defaultTarget: string, projectUid: string, targets: ITargetMap @@ -74,7 +73,7 @@ export const saveChangesToTarget = ( return async (dispatch: any) => { const targetsReference = doc(targetsCollReference, projectUid); const batch = writeBatch(database); - + console.log({ targets, defaultTarget }); try { batch.set( targetsReference, diff --git a/src/components/target-controls/config-dialog/footer.tsx b/src/components/target-controls/config-dialog/footer.tsx new file mode 100644 index 00000000..13d01ebf --- /dev/null +++ b/src/components/target-controls/config-dialog/footer.tsx @@ -0,0 +1,58 @@ +import React from "react"; +import Fab from "@mui/material/Fab"; +import AddIcon from "@mui/icons-material/Add"; +import SaveIcon from "@mui/icons-material/Save"; +import * as SS from "./styles"; + +interface TargetControlsConfigDialogFooterProps { + handleCreateNewTarget: () => void; + handleCloseModal: () => void; + handleSave: () => void; + hasModifiedTargets: boolean; +} + +export const TargetControlsConfigDialogFooter = ({ + handleCreateNewTarget, + handleCloseModal, + handleSave, + hasModifiedTargets +}: TargetControlsConfigDialogFooterProps) => { + return ( +
+ { + handleCreateNewTarget(); + }} + > + Add Target + + + handleSave()} + style={{ marginLeft: 12 }} + > + Save changes + + + handleCloseModal()} + > + Close + +
+ ); +}; diff --git a/src/components/target-controls/config-dialog/index.tsx b/src/components/target-controls/config-dialog/index.tsx new file mode 100644 index 00000000..02584079 --- /dev/null +++ b/src/components/target-controls/config-dialog/index.tsx @@ -0,0 +1,74 @@ +import React from "react"; +import { isEmpty } from "ramda"; +// import CloseIcon from "@mui/icons-material/Close"; +import { hr as hrCss } from "@styles/_common"; +import { TargetControlsConfigDialogFooter } from "./footer"; +import { TargetControlsConfigDialogSingleTarget } from "./single-target"; +import { useTargetControlsDialog } from "./use-target-controls-dialog"; +import * as SS from "./styles"; + +export const TargetControlsConfigDialog = () => { + const { + allDocuments, + handleCloseModal, + handleCreateNewTarget, + handleTargetDelete, + handleTargetNameChange, + handleSelectTargetDocument, + handleMarkAsDefaultTarget, + handleEnableCsound7, + handleSave, + hasModifiedTargets, + newTargets, + targets, + theme + } = useTargetControlsDialog(); + + return ( +
+

Project configuration

+ {(!targets || isEmpty(targets)) && ( +

+ {'No targets found, press "ADD TARGET" to get started. '} +

+ )} + {(newTargets || []).map( + (props, index) => + props.targetDocumentUid && ( + + ) + )} +
+ +
+ ); +}; diff --git a/src/components/target-controls/config-dialog/single-target.tsx b/src/components/target-controls/config-dialog/single-target.tsx new file mode 100644 index 00000000..6137dd1c --- /dev/null +++ b/src/components/target-controls/config-dialog/single-target.tsx @@ -0,0 +1,202 @@ +import React from "react"; +import Select from "react-select"; +import { ascend, either, filter, equals, map, pipe, prop, sort } from "ramda"; +import { useTheme } from "@emotion/react"; +import DeleteIcon from "@mui/icons-material/DeleteTwoTone"; +import Radio from "@mui/material/Radio"; +import Checkbox from "@mui/material/Checkbox"; +import Fab from "@mui/material/Fab"; +import FormControlLabel from "@mui/material/FormControlLabel"; +import FormGroup from "@mui/material/FormGroup"; +import TextField from "@mui/material/TextField"; +import Tooltip from "@mui/material/Tooltip"; +import { IDocument } from "@comp/projects/types"; +import { filenameToCsoundType } from "@comp/csound/utils"; +import { ITargetFromInput } from "../types"; +import { validateTargetName } from "./utils"; +import { reactSelectDropdownStyle } from "../styles"; +import * as SS from "./styles"; + +interface TargetControlsConfigDialogSingleTargetProperties { + allDocuments: IDocument[]; + targetIndex: number; + targetName: string; + oldTargetName: string; + targetDocumentUid: string; + isDefaultTarget: boolean; + useCsound7: boolean; + handleTargetDelete: (targetName: string) => void; + handleTargetNameChange: (props: { + nextValue: string; + oldTargetName: string; + targetIndex: number; + }) => void; + handleSelectTargetDocument: (props: { + nextTargetDocumentUid: string; + targetIndex: number; + }) => void; + handleMarkAsDefaultTarget: (nextTarget: string) => void; + handleEnableCsound7: (props: { + enableCsound7: boolean; + targetIndex: number; + }) => void; + newTargets: ITargetFromInput[]; +} + +export const TargetControlsConfigDialogSingleTarget = ({ + targetIndex, + targetName, + oldTargetName, + targetDocumentUid, + isDefaultTarget, + useCsound7, + handleSelectTargetDocument, + handleTargetNameChange, + handleTargetDelete, + handleMarkAsDefaultTarget, + handleEnableCsound7, + newTargets, + allDocuments +}: TargetControlsConfigDialogSingleTargetProperties) => { + const theme = useTheme(); + const targetNameIsValid = validateTargetName({ + targetName, + oldTargetName, + newTargets + }); + + const targetDocument = allDocuments.find( + (doc: IDocument) => doc.documentUid === targetDocumentUid + ); + + return ( +
+ + + + !isDefaultTarget && + handleMarkAsDefaultTarget(targetName) + } + /> + } + label="Mark as default target" + /> + + + + handleTargetDelete(targetName)} + css={SS.closeIcon} + color="secondary" + variant="circular" + size="small" + disabled={newTargets.length === 1} + > + 1 + ? { fill: "white" } + : {} + } + /> + + + + + + + + handleTargetNameChange({ + oldTargetName, + nextValue: event.target.value, + targetIndex + }) + } + /> + + +
+

+ {"selected csound document to play"} +

+ - {targetType === "main" && ( -
-

{"main document"}

-