diff options
26 files changed, 3240 insertions, 616 deletions
diff --git a/package-lock.json b/package-lock.json index 176d1f27e..5449a9d69 100644 --- a/package-lock.json +++ b/package-lock.json @@ -283,14 +283,14 @@ "integrity": "sha512-fGCl/gwB7jmKCRI2FhgIBeIa/LCSUcjlEcckH2Bc2dIjhJ+2nspp+22lubxcseN6jjrmP42kkXt/reAPe+KJkQ==" }, "@tensorflow/tfjs-converter": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-converter/-/tfjs-converter-1.7.2.tgz", - "integrity": "sha512-6ckNQ3n36X20/+YDCTictfKftPH3urf5I+wc0QNlsSJhik3QiliYJy+v5hmjQGSJ4ow2oyW5T2lUsy9aNoNaTg==" + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-converter/-/tfjs-converter-1.7.4.tgz", + "integrity": "sha512-B/Ux9I3osI0CXoESGR0Xe5C6BsEfC04+g2xn5zVaW9KEuVEnGEgnuBQxgijRFzkqTwoyLv4ptAmjyIghVARX0Q==" }, "@tensorflow/tfjs-core": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-core/-/tfjs-core-1.7.2.tgz", - "integrity": "sha512-2o5jjXLHrMY4f+r7aOfJZbKqrchEYrRk2mhlFD3RgKqtSKUpfv15YLQr4uc0uMbNSwuZbjWwrmaL9yyaBWYSAQ==", + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-core/-/tfjs-core-1.7.4.tgz", + "integrity": "sha512-3G4VKJ6nPs7iCt6gs3bjRj8chihKrYWenf63R0pm7D9MhlrVoX/tpN4LYVMGgBL7jHPxMLKdOkoAZJrn/J88HQ==", "requires": { "@types/offscreencanvas": "~2019.3.0", "@types/seedrandom": "2.4.27", @@ -6300,9 +6300,9 @@ } }, "google-maps-react": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/google-maps-react/-/google-maps-react-2.0.2.tgz", - "integrity": "sha512-6cYauGwt22haDUrWxKQ6yoNOqjiuxHo8YYcmb+aBvNICokdXmZOUB6Ah4vD5VexMVlrwP2PFqA/D8sHpEB52KA==" + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/google-maps-react/-/google-maps-react-2.0.6.tgz", + "integrity": "sha512-M8Eo9WndfQEfxcmm6yRq03qdJgw1x6rQmJ9DN+a+xPQ3K7yNDGkVDbinrf4/8vcox7nELbeopbm4bpefKewWfQ==" }, "google-p12-pem": { "version": "2.0.4", @@ -6809,9 +6809,9 @@ } }, "ibm-watson": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/ibm-watson/-/ibm-watson-5.4.0.tgz", - "integrity": "sha512-rgM42p/XL12yYSOvrmgp+NYZlaNdaIeyKl8TlF982J8dEnkXgYhYF7k05Fg1nZ0ZZ80AF1Fja/PW2mB53AjNGA==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/ibm-watson/-/ibm-watson-5.5.0.tgz", + "integrity": "sha512-gcijKSFTOMLcjEnSpeUa4/31naeMSZsz8t/wYNrR5c8gp1VN9LbJ3vhn3nFislfLYopTkNnWzya7w8oMhW4SGg==", "requires": { "@types/async": "^2.4.2", "@types/extend": "^3.0.1", @@ -6828,9 +6828,9 @@ }, "dependencies": { "@types/node": { - "version": "11.15.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-11.15.10.tgz", - "integrity": "sha512-FHli7aK/ViA02vbWKmiNejJW4BCJxCVb6macS5gi71fST+UrrdqcES6Lh5rx23hU1QCBeUNXfPquYr9jJv7FuA==" + "version": "11.15.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-11.15.12.tgz", + "integrity": "sha512-iefeBfpmhoYaZfj+gJM5z9H9eiTwhuzhPsJgH/flx4HP2SBI2FNDra1D3vKljqPLGDr9Cazvh9gP9Xszc30ncA==" } } }, diff --git a/report.20190510.131414.316.001.json b/report.20190510.131414.316.001.json deleted file mode 100644 index 390db8ba6..000000000 --- a/report.20190510.131414.316.001.json +++ /dev/null @@ -1,592 +0,0 @@ - -{ - "header": { - "event": "Allocation failed - JavaScript heap out of memory", - "trigger": "FatalError", - "filename": "report.20190510.131414.316.001.json", - "dumpEventTime": "2019-05-10T13:14:14Z", - "processId": 316, - "commandLine": [ - "C:\\Program Files\\nodejs\\node.exe", - "--max-old-space-size=2048", - "C:\\Users\\dash\\Documents\\Dash-Web\\node_modules\\ts-node-dev\\lib\\wrap.js", - "C:\\Users\\dash\\Documents\\Dash-Web\\node_modules\\fork-ts-checker-webpack-plugin\\lib\\service.js" - ], - "nodejsVersion": "v11.11.0", - "wordSize": 64, - "arch": "x64", - "platform": "win32", - "componentVersions": { - "node": "11.11.0", - "v8": "7.0.276.38-node.18", - "uv": "1.26.0", - "zlib": "1.2.11", - "brotli": "1.0.7", - "ares": "1.15.0", - "modules": "67", - "nghttp2": "1.34.0", - "napi": "4", - "llhttp": "1.1.1", - "http_parser": "2.8.0", - "openssl": "1.1.1a", - "cldr": "34.0", - "icu": "63.1", - "tz": "2018e", - "unicode": "11.0" - }, - "release": { - "name": "node", - "headersUrl": "https://nodejs.org/download/release/v11.11.0/node-v11.11.0-headers.tar.gz", - "sourceUrl": "https://nodejs.org/download/release/v11.11.0/node-v11.11.0.tar.gz", - "libUrl": "https://nodejs.org/download/release/v11.11.0/win-x64/node.lib" - }, - "osName": "Windows_NT", - "osRelease": "10.0.14393", - "osVersion": "Windows Server 2016 Datacenter", - "osMachine": "x86_64", - "host": "dashserver" - }, - "javascriptStack": { - "message": "No stack.", - "stack": [ - "Unavailable." - ] - }, - "nativeStack": [ - { - "pc": "0x00007ff6d814bc4e", - "symbol": "SSL_SESSION_get0_peer+15022" - }, - { - "pc": "0x00007ff6d814ab04", - "symbol": "SSL_SESSION_get0_peer+10596" - }, - { - "pc": "0x00007ff6d814a336", - "symbol": "SSL_SESSION_get0_peer+8598" - }, - { - "pc": "0x00007ff6d823703a", - "symbol": "uv_loop_fork+86202" - }, - { - "pc": "0x00007ff6d861e49e", - "symbol": "v8::internal::FatalProcessOutOfMemory+798" - }, - { - "pc": "0x00007ff6d861e3d7", - "symbol": "v8::internal::FatalProcessOutOfMemory+599" - }, - { - "pc": "0x00007ff6d86c91a4", - "symbol": "v8::internal::Heap::RootIsImmortalImmovable+14068" - }, - { - "pc": "0x00007ff6d86befc8", - "symbol": "v8::internal::Heap::CollectGarbage+7256" - }, - { - "pc": "0x00007ff6d86bd7c8", - "symbol": "v8::internal::Heap::CollectGarbage+1112" - }, - { - "pc": "0x00007ff6d86c70f7", - "symbol": "v8::internal::Heap::RootIsImmortalImmovable+5703" - }, - { - "pc": "0x00007ff6d86c7176", - "symbol": "v8::internal::Heap::RootIsImmortalImmovable+5830" - }, - { - "pc": "0x00007ff6d8848481", - "symbol": "v8::internal::Factory::NewFillerObject+49" - }, - { - "pc": "0x00007ff6d88fdbb6", - "symbol": "v8::internal::StoreBuffer::StoreBufferOverflow+27190" - }, - { - "pc": "0x0000033e2e4d0481", - "symbol": "" - } - ], - "javascriptHeap": { - "totalMemory": 380211200, - "totalCommittedMemory": 380211200, - "usedMemory": 365246984, - "availableMemory": 1844483352, - "memoryLimit": 2217857988, - "heapSpaces": { - "read_only_space": { - "memorySize": 524288, - "committedMemory": 524288, - "capacity": 515584, - "used": 33520, - "available": 482064 - }, - "new_space": { - "memorySize": 1048576, - "committedMemory": 1048576, - "capacity": 1031168, - "used": 1031088, - "available": 80 - }, - "old_space": { - "memorySize": 341512192, - "committedMemory": 341512192, - "capacity": 335760416, - "used": 329680232, - "available": 6080184 - }, - "code_space": { - "memorySize": 4194304, - "committedMemory": 4194304, - "capacity": 3775360, - "used": 3762688, - "available": 12672 - }, - "map_space": { - "memorySize": 1585152, - "committedMemory": 1585152, - "capacity": 1550096, - "used": 852880, - "available": 697216 - }, - "large_object_space": { - "memorySize": 31346688, - "committedMemory": 31346688, - "capacity": 1867097712, - "used": 29886576, - "available": 1837211136 - }, - "new_large_object_space": { - "memorySize": 0, - "committedMemory": 0, - "capacity": 0, - "used": 0, - "available": 0 - } - } - }, - "libuv": [ - ], - "environmentVariables": { - "ACLOCAL_PATH": "C:\\Program Files\\Git\\mingw64\\share\\aclocal;C:\\Program Files\\Git\\usr\\share\\aclocal", - "ALLUSERSPROFILE": "C:\\ProgramData", - "APPDATA": "C:\\Users\\dash\\AppData\\Roaming", - "CHECK_SYNTACTIC_ERRORS": "false", - "CLIENTNAME": "DESKTOP-7P5JHEB", - "COMMONPROGRAMFILES": "C:\\Program Files\\Common Files", - "CommonProgramFiles(x86)": "C:\\Program Files (x86)\\Common Files", - "CommonProgramW6432": "C:\\Program Files\\Common Files", - "COMPILER_OPTIONS": "{}", - "COMPUTERNAME": "dashserver", - "COMSPEC": "C:\\Windows\\system32\\cmd.exe", - "CONFIG_SITE": "C:/Program Files/Git/mingw64/etc/config.site", - "CONTEXT": "C:\\Users\\dash\\Documents\\Dash-Web", - "DISPLAY": "needs-to-be-defined", - "EXEPATH": "C:\\Program Files\\Git", - "FPS_BROWSER_APP_PROFILE_STRING": "Internet Explorer", - "FPS_BROWSER_USER_PROFILE_STRING": "Default", - "HOME": "C:\\Users\\dash", - "HOMEDRIVE": "C:", - "HOMEPATH": "\\Users\\dash", - "HOSTNAME": "dashserver", - "INFOPATH": "C:\\Program Files\\Git\\usr\\local\\info;C:\\Program Files\\Git\\usr\\share\\info;C:\\Program Files\\Git\\usr\\info;C:\\Program Files\\Git\\share\\info", - "INIT_CWD": "C:\\Users\\dash\\Documents\\Dash-Web", - "LANG": "en_US.UTF-8", - "LOCALAPPDATA": "C:\\Users\\dash\\AppData\\Local", - "LOGONSERVER": "\\\\dashserver", - "MANPATH": "C:\\Program Files\\Git\\mingw64\\local\\man;C:\\Program Files\\Git\\mingw64\\share\\man;C:\\Program Files\\Git\\usr\\local\\man;C:\\Program Files\\Git\\usr\\share\\man;C:\\Program Files\\Git\\usr\\man;C:\\Program Files\\Git\\share\\man", - "MEMORY_LIMIT": "2048", - "MINGW_CHOST": "x86_64-w64-mingw32", - "MINGW_PACKAGE_PREFIX": "mingw-w64-x86_64", - "MINGW_PREFIX": "C:/Program Files/Git/mingw64", - "MSYSTEM": "MINGW64", - "MSYSTEM_CARCH": "x86_64", - "MSYSTEM_CHOST": "x86_64-w64-mingw32", - "MSYSTEM_PREFIX": "C:/Program Files/Git/mingw64", - "NODE": "C:\\Program Files\\nodejs\\node.exe", - "NODE_ENV": "development", - "npm_config_access": "", - "npm_config_allow_same_version": "", - "npm_config_also": "", - "npm_config_always_auth": "", - "npm_config_argv": "{\"remain\":[],\"cooked\":[\"start\"],\"original\":[\"start\"]}", - "npm_config_audit": "true", - "npm_config_audit_level": "low", - "npm_config_auth_type": "legacy", - "npm_config_bin_links": "true", - "npm_config_browser": "", - "npm_config_ca": "", - "npm_config_cache": "C:\\Users\\dash\\AppData\\Roaming\\npm-cache", - "npm_config_cache_lock_retries": "10", - "npm_config_cache_lock_stale": "60000", - "npm_config_cache_lock_wait": "10000", - "npm_config_cache_max": "Infinity", - "npm_config_cache_min": "10", - "npm_config_cafile": "", - "npm_config_cert": "", - "npm_config_cidr": "", - "npm_config_color": "true", - "npm_config_commit_hooks": "true", - "npm_config_depth": "Infinity", - "npm_config_description": "true", - "npm_config_dev": "", - "npm_config_dry_run": "", - "npm_config_editor": "notepad.exe", - "npm_config_engine_strict": "", - "npm_config_fetch_retries": "2", - "npm_config_fetch_retry_factor": "10", - "npm_config_fetch_retry_maxtimeout": "60000", - "npm_config_fetch_retry_mintimeout": "10000", - "npm_config_force": "", - "npm_config_git": "git", - "npm_config_git_tag_version": "true", - "npm_config_global": "", - "npm_config_globalconfig": "C:\\Users\\dash\\AppData\\Roaming\\npm\\etc\\npmrc", - "npm_config_globalignorefile": "C:\\Users\\dash\\AppData\\Roaming\\npm\\etc\\npmignore", - "npm_config_global_style": "", - "npm_config_group": "", - "npm_config_ham_it_up": "", - "npm_config_heading": "npm", - "npm_config_https_proxy": "", - "npm_config_if_present": "", - "npm_config_ignore_prepublish": "", - "npm_config_ignore_scripts": "", - "npm_config_init_author_email": "", - "npm_config_init_author_name": "", - "npm_config_init_author_url": "", - "npm_config_init_license": "ISC", - "npm_config_init_module": "C:\\Users\\dash\\.npm-init.js", - "npm_config_init_version": "1.0.0", - "npm_config_json": "", - "npm_config_key": "", - "npm_config_legacy_bundling": "", - "npm_config_link": "", - "npm_config_local_address": "", - "npm_config_loglevel": "notice", - "npm_config_logs_max": "10", - "npm_config_long": "", - "npm_config_maxsockets": "50", - "npm_config_message": "%s", - "npm_config_metrics_registry": "https://registry.npmjs.org/", - "npm_config_node_gyp": "C:\\Program Files\\nodejs\\node_modules\\npm\\node_modules\\node-gyp\\bin\\node-gyp.js", - "npm_config_node_options": "", - "npm_config_node_version": "11.11.0", - "npm_config_noproxy": "", - "npm_config_offline": "", - "npm_config_onload_script": "", - "npm_config_only": "", - "npm_config_optional": "true", - "npm_config_otp": "", - "npm_config_package_lock": "true", - "npm_config_package_lock_only": "", - "npm_config_parseable": "", - "npm_config_prefer_offline": "", - "npm_config_prefer_online": "", - "npm_config_prefix": "C:\\Users\\dash\\AppData\\Roaming\\npm", - "npm_config_preid": "", - "npm_config_production": "", - "npm_config_progress": "true", - "npm_config_proxy": "", - "npm_config_read_only": "", - "npm_config_rebuild_bundle": "true", - "npm_config_registry": "https://registry.npmjs.org/", - "npm_config_rollback": "true", - "npm_config_save": "true", - "npm_config_save_bundle": "", - "npm_config_save_dev": "", - "npm_config_save_exact": "", - "npm_config_save_optional": "", - "npm_config_save_prefix": "^", - "npm_config_save_prod": "", - "npm_config_scope": "", - "npm_config_scripts_prepend_node_path": "warn-only", - "npm_config_script_shell": "", - "npm_config_searchexclude": "", - "npm_config_searchlimit": "20", - "npm_config_searchopts": "", - "npm_config_searchstaleness": "900", - "npm_config_send_metrics": "", - "npm_config_shell": "C:\\Windows\\system32\\cmd.exe", - "npm_config_shrinkwrap": "true", - "npm_config_sign_git_commit": "", - "npm_config_sign_git_tag": "", - "npm_config_sso_poll_frequency": "500", - "npm_config_sso_type": "oauth", - "npm_config_strict_ssl": "true", - "npm_config_tag": "latest", - "npm_config_tag_version_prefix": "v", - "npm_config_timing": "", - "npm_config_tmp": "C:\\Users\\dash\\AppData\\Local\\Temp\\2", - "npm_config_umask": "0000", - "npm_config_unicode": "", - "npm_config_unsafe_perm": "true", - "npm_config_update_notifier": "true", - "npm_config_usage": "", - "npm_config_user": "", - "npm_config_userconfig": "C:\\Users\\dash\\.npmrc", - "npm_config_user_agent": "npm/6.7.0 node/v11.11.0 win32 x64", - "npm_config_version": "", - "npm_config_versions": "", - "npm_config_viewer": "browser", - "npm_execpath": "C:\\Program Files\\nodejs\\node_modules\\npm\\bin\\npm-cli.js", - "npm_lifecycle_event": "start", - "npm_lifecycle_script": "ts-node-dev -- src/server/index.ts", - "npm_node_execpath": "C:\\Program Files\\nodejs\\node.exe", - "npm_package_dependencies_async": "^2.6.2", - "npm_package_dependencies_babel_runtime": "^6.26.0", - "npm_package_dependencies_bcrypt_nodejs": "0.0.3", - "npm_package_dependencies_bluebird": "^3.5.3", - "npm_package_dependencies_body_parser": "^1.18.3", - "npm_package_dependencies_bootstrap": "^4.3.1", - "npm_package_dependencies_class_transformer": "^0.2.0", - "npm_package_dependencies_connect_flash": "^0.1.1", - "npm_package_dependencies_connect_mongo": "^2.0.3", - "npm_package_dependencies_cookie_parser": "^1.4.4", - "npm_package_dependencies_cookie_session": "^2.0.0-beta.3", - "npm_package_dependencies_crypto_browserify": "^3.11.0", - "npm_package_dependencies_d3_format": "^1.3.2", - "npm_package_dependencies_express": "^4.16.4", - "npm_package_dependencies_expressjs": "^1.0.1", - "npm_package_dependencies_express_flash": "0.0.2", - "npm_package_dependencies_express_session": "^1.15.6", - "npm_package_dependencies_express_validator": "^5.3.1", - "npm_package_dependencies_flexlayout_react": "^0.3.3", - "npm_package_dependencies_font_awesome": "^4.7.0", - "npm_package_dependencies_formidable": "^1.2.1", - "npm_package_dependencies_golden_layout": "^1.5.9", - "npm_package_dependencies_html_to_image": "^0.1.0", - "npm_package_dependencies_i": "^0.3.6", - "npm_package_dependencies_jsonwebtoken": "^8.5.0", - "npm_package_dependencies_jsx_to_string": "^1.4.0", - "npm_package_dependencies_lodash": "^4.17.11", - "npm_package_dependencies_mobile_detect": "^1.4.3", - "npm_package_dependencies_mobx": "^5.9.0", - "npm_package_dependencies_mobx_react": "^5.3.5", - "npm_package_dependencies_mobx_react_devtools": "^6.1.1", - "npm_package_dependencies_mongodb": "^3.1.13", - "npm_package_dependencies_mongoose": "^5.4.18", - "npm_package_dependencies_nodemailer": "^5.1.1", - "npm_package_dependencies_nodemon": "^1.18.10", - "npm_package_dependencies_node_sass": "^4.12.0", - "npm_package_dependencies_normalize_css": "^8.0.1", - "npm_package_dependencies_npm": "^6.9.0", - "npm_package_dependencies_passport": "^0.4.0", - "npm_package_dependencies_passport_local": "^1.0.0", - "npm_package_dependencies_prosemirror_commands": "^1.0.7", - "npm_package_dependencies_prosemirror_example_setup": "^1.0.1", - "npm_package_dependencies_prosemirror_history": "^1.0.4", - "npm_package_dependencies_prosemirror_keymap": "^1.0.1", - "npm_package_dependencies_prosemirror_model": "^1.7.0", - "npm_package_dependencies_prosemirror_schema_basic": "^1.0.0", - "npm_package_dependencies_prosemirror_schema_list": "^1.0.2", - "npm_package_dependencies_prosemirror_state": "^1.2.2", - "npm_package_dependencies_prosemirror_transform": "^1.1.3", - "npm_package_dependencies_prosemirror_view": "^1.8.3", - "npm_package_dependencies_pug": "^2.0.3", - "npm_package_dependencies_raw_loader": "^1.0.0", - "npm_package_dependencies_react": "^16.8.4", - "npm_package_dependencies_react_bootstrap": "^1.0.0-beta.5", - "npm_package_dependencies_react_bootstrap_dropdown_menu": "^1.1.15", - "npm_package_dependencies_react_color": "^2.17.0", - "npm_package_dependencies_react_dimensions": "^1.3.1", - "npm_package_dependencies_react_dom": "^16.8.4", - "npm_package_dependencies_react_golden_layout": "^1.0.6", - "npm_package_dependencies_react_image_lightbox": "^5.1.0", - "npm_package_dependencies_react_jsx_parser": "^1.15.0", - "npm_package_dependencies_react_measure": "^2.2.4", - "npm_package_dependencies_react_mosaic": "0.0.20", - "npm_package_dependencies_react_pdf": "^4.0.2", - "npm_package_dependencies_react_pdf_highlighter": "^2.1.2", - "npm_package_dependencies_react_pdf_js": "^4.0.2", - "npm_package_dependencies_react_simple_dropdown": "^3.2.3", - "npm_package_dependencies_react_split_pane": "^0.1.85", - "npm_package_dependencies_react_table": "^6.9.2", - "npm_package_dependencies_request": "^2.88.0", - "npm_package_dependencies_request_promise": "^4.2.4", - "npm_package_dependencies_serializr": "^1.5.1", - "npm_package_dependencies_socket_io": "^2.2.0", - "npm_package_dependencies_socket_io_client": "^2.2.0", - "npm_package_dependencies_solr_node": "^1.1.3", - "npm_package_dependencies_typescript_collections": "^1.3.2", - "npm_package_dependencies_url_loader": "^1.1.2", - "npm_package_dependencies_uuid": "^3.3.2", - "npm_package_dependencies_xoauth2": "^1.2.0", - "npm_package_dependencies__fortawesome_fontawesome_free_solid": "^5.0.13", - "npm_package_dependencies__fortawesome_fontawesome_svg_core": "^1.2.15", - "npm_package_dependencies__fortawesome_free_solid_svg_icons": "^5.7.2", - "npm_package_dependencies__fortawesome_react_fontawesome": "^0.1.4", - "npm_package_dependencies__hig_flyout": "^1.0.3", - "npm_package_dependencies__hig_theme_context": "^2.1.3", - "npm_package_dependencies__hig_theme_data": "^2.3.3", - "npm_package_dependencies__trendmicro_react_dropdown": "^1.3.0", - "npm_package_dependencies__types_async": "^2.4.1", - "npm_package_dependencies__types_bcrypt_nodejs": "0.0.30", - "npm_package_dependencies__types_bluebird": "^3.5.25", - "npm_package_dependencies__types_body_parser": "^1.17.0", - "npm_package_dependencies__types_connect_flash": "0.0.34", - "npm_package_dependencies__types_cookie_parser": "^1.4.1", - "npm_package_dependencies__types_cookie_session": "^2.0.36", - "npm_package_dependencies__types_d3_format": "^1.3.1", - "npm_package_dependencies__types_express": "^4.16.1", - "npm_package_dependencies__types_express_flash": "0.0.0", - "npm_package_dependencies__types_express_session": "^1.15.12", - "npm_package_dependencies__types_express_validator": "^3.0.0", - "npm_package_dependencies__types_formidable": "^1.0.31", - "npm_package_dependencies__types_jquery": "^3.3.29", - "npm_package_dependencies__types_jsonwebtoken": "^8.3.2", - "npm_package_dependencies__types_lodash": "^4.14.121", - "npm_package_dependencies__types_mobile_detect": "^1.3.4", - "npm_package_dependencies__types_mongodb": "^3.1.22", - "npm_package_dependencies__types_mongoose": "^5.3.21", - "npm_package_dependencies__types_node": "^10.12.30", - "npm_package_dependencies__types_nodemailer": "^4.6.6", - "npm_package_dependencies__types_passport": "^1.0.0", - "npm_package_dependencies__types_passport_local": "^1.0.33", - "npm_package_dependencies__types_prosemirror_commands": "^1.0.1", - "npm_package_dependencies__types_prosemirror_history": "^1.0.1", - "npm_package_dependencies__types_prosemirror_inputrules": "^1.0.2", - "npm_package_dependencies__types_prosemirror_keymap": "^1.0.1", - "npm_package_dependencies__types_prosemirror_menu": "^1.0.1", - "npm_package_dependencies__types_prosemirror_model": "^1.7.0", - "npm_package_dependencies__types_prosemirror_schema_basic": "^1.0.1", - "npm_package_dependencies__types_prosemirror_schema_list": "^1.0.1", - "npm_package_dependencies__types_prosemirror_state": "^1.2.3", - "npm_package_dependencies__types_prosemirror_transform": "^1.1.0", - "npm_package_dependencies__types_prosemirror_view": "^1.3.1", - "npm_package_dependencies__types_pug": "^2.0.4", - "npm_package_dependencies__types_react": "^16.8.7", - "npm_package_dependencies__types_react_color": "^2.14.1", - "npm_package_dependencies__types_react_measure": "^2.0.4", - "npm_package_dependencies__types_react_table": "^6.7.22", - "npm_package_dependencies__types_request": "^2.48.1", - "npm_package_dependencies__types_request_promise": "^4.1.42", - "npm_package_dependencies__types_socket_io": "^2.1.2", - "npm_package_dependencies__types_socket_io_client": "^1.4.32", - "npm_package_dependencies__types_typescript": "^2.0.0", - "npm_package_dependencies__types_uuid": "^3.4.4", - "npm_package_dependencies__types_webpack": "^4.4.25", - "npm_package_description": "Install Node.js, then, from the project directory, run", - "npm_package_devDependencies_awesome_typescript_loader": "^5.2.1", - "npm_package_devDependencies_chai": "^4.2.0", - "npm_package_devDependencies_copy_webpack_plugin": "^4.6.0", - "npm_package_devDependencies_css_loader": "^2.1.1", - "npm_package_devDependencies_file_loader": "^3.0.1", - "npm_package_devDependencies_fork_ts_checker_webpack_plugin": "^1.0.2", - "npm_package_devDependencies_mocha": "^5.2.0", - "npm_package_devDependencies_sass_loader": "^7.1.0", - "npm_package_devDependencies_scss_loader": "0.0.1", - "npm_package_devDependencies_style_loader": "^0.23.1", - "npm_package_devDependencies_tslint": "^5.15.0", - "npm_package_devDependencies_tslint_loader": "^3.5.4", - "npm_package_devDependencies_ts_loader": "^5.3.3", - "npm_package_devDependencies_ts_node": "^7.0.1", - "npm_package_devDependencies_ts_node_dev": "^1.0.0-pre.32", - "npm_package_devDependencies_typescript": "^3.4.1", - "npm_package_devDependencies_webpack": "^4.29.6", - "npm_package_devDependencies_webpack_cli": "^3.2.3", - "npm_package_devDependencies_webpack_dev_middleware": "^3.6.1", - "npm_package_devDependencies_webpack_dev_server": "^3.3.1", - "npm_package_devDependencies_webpack_hot_middleware": "^2.24.3", - "npm_package_devDependencies__types_chai": "^4.1.7", - "npm_package_devDependencies__types_mocha": "^5.2.6", - "npm_package_devDependencies__types_react_dom": "^16.8.2", - "npm_package_devDependencies__types_webpack_dev_middleware": "^2.0.2", - "npm_package_devDependencies__types_webpack_hot_middleware": "^2.16.4", - "npm_package_gitHead": "f38d2de249ddf2cad533e2f92197738835688a73", - "npm_package_main": "index.js", - "npm_package_name": "dash", - "npm_package_readmeFilename": "README.md", - "npm_package_scripts_build": "webpack --env production", - "npm_package_scripts_debug": "ts-node-dev --inspect -- src/server/index.ts", - "npm_package_scripts_start": "ts-node-dev -- src/server/index.ts", - "npm_package_scripts_test": "mocha -r ts-node/register test/**/*.ts", - "npm_package_scripts_tsc": "tsc", - "npm_package_version": "1.0.0", - "NUMBER_OF_PROCESSORS": "1", - "OLDPWD": "C:/Program Files/Git/", - "ORIGINAL_PATH": "C:\\Program Files\\Git\\mingw64\\bin;C:\\Program Files\\Git\\usr\\bin;C:\\Users\\dash\\bin;C:\\Windows\\system32;C:\\Windows;C:\\Windows\\System32\\Wbem;C:\\Windows\\System32\\WindowsPowerShell\\v1.0;C:\\Program Files\\Microsoft\\Web Platform Installer;C:\\Program Files\\Microsoft SQL Server\\130\\Tools\\Binn;C:\\Program Files\\dotnet;C:\\Program Files\\nodejs;C:\\Users\\dash\\AppData\\Local\\Microsoft\\WindowsApps;C:\\Users\\dash\\AppData\\Roaming\\npm", - "ORIGINAL_TEMP": "C:/Users/dash/AppData/Local/Temp/2", - "ORIGINAL_TMP": "C:/Users/dash/AppData/Local/Temp/2", - "OS": "Windows_NT", - "PATH": "C:\\Program Files\\nodejs\\node_modules\\npm\\node_modules\\npm-lifecycle\\node-gyp-bin;C:\\Users\\dash\\Documents\\Dash-Web\\node_modules\\.bin;C:\\Users\\dash\\bin;C:\\Program Files\\Git\\mingw64\\bin;C:\\Program Files\\Git\\usr\\local\\bin;C:\\Program Files\\Git\\usr\\bin;C:\\Program Files\\Git\\usr\\bin;C:\\Program Files\\Git\\mingw64\\bin;C:\\Program Files\\Git\\usr\\bin;C:\\Users\\dash\\bin;C:\\Windows\\system32;C:\\Windows;C:\\Windows\\System32\\Wbem;C:\\Windows\\System32\\WindowsPowerShell\\v1.0;C:\\Program Files\\Microsoft\\Web Platform Installer;C:\\Program Files\\Microsoft SQL Server\\130\\Tools\\Binn;C:\\Program Files\\dotnet;C:\\Program Files\\nodejs;C:\\Users\\dash\\AppData\\Local\\Microsoft\\WindowsApps;C:\\Users\\dash\\AppData\\Roaming\\npm;C:\\Program Files\\Git\\usr\\bin\\vendor_perl;C:\\Program Files\\Git\\usr\\bin\\core_perl", - "PATHEXT": ".COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JSE;.WSF;.WSH;.MSC", - "PKG_CONFIG_PATH": "C:\\Program Files\\Git\\mingw64\\lib\\pkgconfig;C:\\Program Files\\Git\\mingw64\\share\\pkgconfig", - "PLINK_PROTOCOL": "ssh", - "PROCESSOR_ARCHITECTURE": "AMD64", - "PROCESSOR_IDENTIFIER": "Intel64 Family 6 Model 63 Stepping 2, GenuineIntel", - "PROCESSOR_LEVEL": "6", - "PROCESSOR_REVISION": "3f02", - "ProgramData": "C:\\ProgramData", - "PROGRAMFILES": "C:\\Program Files", - "ProgramFiles(x86)": "C:\\Program Files (x86)", - "ProgramW6432": "C:\\Program Files", - "PROMPT": "$P$G", - "PSModulePath": "C:\\Program Files\\WindowsPowerShell\\Modules;C:\\Windows\\system32\\WindowsPowerShell\\v1.0\\Modules;C:\\Program Files\\WindowsPowerShell\\Modules\\;C:\\Program Files (x86)\\Microsoft SDKs\\Azure\\PowerShell\\ResourceManager\\AzureResourceManager\\;C:\\Program Files (x86)\\Microsoft SDKs\\Azure\\PowerShell\\ServiceManagement\\;C:\\Program Files (x86)\\Microsoft SDKs\\Azure\\PowerShell\\Storage\\", - "PUBLIC": "C:\\Users\\Public", - "PWD": "C:/Users/dash/Documents/Dash-Web", - "SESSIONNAME": "RDP-Tcp#0", - "SHELL": "C:\\Program Files\\Git\\usr\\bin\\bash.exe", - "SHLVL": "2", - "SSH_ASKPASS": "C:/Program Files/Git/mingw64/libexec/git-core/git-gui--askpass", - "SYSTEMDRIVE": "C:", - "SYSTEMROOT": "C:\\Windows", - "TEMP": "C:\\Users\\dash\\AppData\\Local\\Temp\\2", - "TERM": "xterm", - "TMP": "C:\\Users\\dash\\AppData\\Local\\Temp\\2", - "TMPDIR": "C:\\Users\\dash\\AppData\\Local\\Temp\\2", - "TSCONFIG": "C:\\Users\\dash\\Documents\\Dash-Web\\tsconfig.json", - "TSLINT": "true", - "TSLINTAUTOFIX": "false", - "TYPESCRIPT_PATH": "C:\\Users\\dash\\Documents\\Dash-Web\\node_modules\\typescript\\lib\\typescript.js", - "USERDOMAIN": "dashserver", - "USERDOMAIN_ROAMINGPROFILE": "dashserver", - "USERNAME": "dash", - "USERPROFILE": "C:\\Users\\dash", - "USE_INCREMENTAL_API": "true", - "VUE": "false", - "WATCH": "", - "WINDIR": "C:\\Windows", - "WORK_DIVISION": "1", - "_": "C:/Program Files/nodejs/node.exe" - }, - "sharedObjects": [ - "C:\\Program Files\\nodejs\\node.exe", - "C:\\Windows\\SYSTEM32\\ntdll.dll", - "C:\\Windows\\System32\\KERNEL32.DLL", - "C:\\Windows\\System32\\KERNELBASE.dll", - "C:\\Windows\\System32\\PSAPI.DLL", - "C:\\Windows\\System32\\WS2_32.dll", - "C:\\Windows\\System32\\sechost.dll", - "C:\\Windows\\System32\\RPCRT4.dll", - "C:\\Windows\\System32\\ADVAPI32.dll", - "C:\\Windows\\System32\\msvcrt.dll", - "C:\\Windows\\System32\\USER32.dll", - "C:\\Windows\\System32\\win32u.dll", - "C:\\Windows\\System32\\GDI32.dll", - "C:\\Windows\\System32\\gdi32full.dll", - "C:\\Windows\\System32\\CRYPT32.dll", - "C:\\Windows\\System32\\ucrtbase.dll", - "C:\\Windows\\System32\\MSASN1.dll", - "C:\\Windows\\SYSTEM32\\dbghelp.dll", - "C:\\Windows\\SYSTEM32\\IPHLPAPI.DLL", - "C:\\Windows\\SYSTEM32\\USERENV.dll", - "C:\\Windows\\System32\\profapi.dll", - "C:\\Windows\\SYSTEM32\\bcrypt.dll", - "C:\\Windows\\SYSTEM32\\WINMM.dll", - "C:\\Windows\\SYSTEM32\\WINMMBASE.dll", - "C:\\Windows\\System32\\cfgmgr32.dll", - "C:\\Windows\\System32\\IMM32.DLL", - "C:\\Windows\\System32\\powrprof.dll", - "C:\\Windows\\system32\\mswsock.dll", - "C:\\Windows\\System32\\kernel.appcore.dll", - "C:\\Windows\\System32\\bcryptprimitives.dll", - "C:\\Windows\\system32\\napinsp.dll", - "C:\\Windows\\SYSTEM32\\DNSAPI.dll", - "C:\\Windows\\System32\\NSI.dll", - "C:\\Windows\\System32\\winrnr.dll", - "C:\\Windows\\system32\\NLAapi.dll" - ] -}
\ No newline at end of file diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index f96e3bcd1..811bb5fb2 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -1014,3 +1014,4 @@ export namespace DocUtils { } Scripting.addGlobal("Docs", Docs); + diff --git a/src/client/views/ContextMenu.scss b/src/client/views/ContextMenu.scss index 30938688d..1bf242d93 100644 --- a/src/client/views/ContextMenu.scss +++ b/src/client/views/ContextMenu.scss @@ -7,6 +7,7 @@ box-shadow: $intermediate-color 0.2vw 0.2vw 0.4vw; flex-direction: column; background: whitesmoke; + padding-top: 10px; padding-bottom: 10px; border-radius: 15px; border: solid #BBBBBBBB 1px; diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 20238985d..be46e0107 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -42,6 +42,7 @@ import { OverlayView } from './OverlayView'; import PDFMenu from './pdf/PDFMenu'; import { PreviewCursor } from './PreviewCursor'; import { ScriptField } from '../../new_fields/ScriptField'; +import { TimelineMenu } from './animationtimeline/TimelineMenu'; @observer export class MainView extends React.Component { @@ -163,6 +164,9 @@ export class MainView extends React.Component { if (targets && targets.length && targets[0].className.toString().indexOf("contextMenu") === -1) { ContextMenu.Instance.closeMenu(); } + if (targets && (targets.length && targets[0].className.toString() !== "timeline-menu-desc" && targets[0].className.toString() !== "timeline-menu-item" && targets[0].className.toString() !== "timeline-menu-input")) { + TimelineMenu.Instance.closeMenu(); + } }); globalPointerUp = () => this.isPointerDown = false; @@ -580,6 +584,7 @@ export class MainView extends React.Component { <MarqueeOptionsMenu /> <RichTextMenu /> <OverlayView /> + <TimelineMenu /> </div >); } } diff --git a/src/client/views/animationtimeline/Keyframe.scss b/src/client/views/animationtimeline/Keyframe.scss new file mode 100644 index 000000000..84c8de287 --- /dev/null +++ b/src/client/views/animationtimeline/Keyframe.scss @@ -0,0 +1,105 @@ +@import "./../globalCssVariables.scss"; + + +$timelineColor: #9acedf; +$timelineDark: #77a1aa; + +.bar { + height: 100%; + width: 5px; + position: absolute; + + // pointer-events: none; + .menubox { + width: 200px; + height: 200px; + top: 50%; + position: relative; + background-color: $light-color; + + .menutable { + tr:nth-child(odd) { + background-color: $light-color-secondary; + } + } + } + + .leftResize { + left: -10px; + border: 3px solid black; + } + + .rightResize { + right: -10px; + border: 3px solid black; + } + + .keyframe-indicator { + height: 20px; + width: 20px; + top: calc(50% - 10px); + background-color: white; + -ms-transform: rotate(45deg); + -webkit-transform: rotate(45deg); + transform: rotate(45deg); + z-index: 1000; + position: absolute; + } + + .keyframe-information { + display: none; + position: relative; + // z-index: 100000; + // background: $timelineDark; + width: 100px; + // left: -50px; + height: 100px; + // top: 40px; + } + + .keyframeCircle { + left: -10px; + border: 3px solid $timelineDark; + } + + .fadeLeft { + left: 0px; + height: 100%; + position: absolute; + pointer-events: none; + background: linear-gradient(to left, $timelineColor 10%, $light-color); + } + + .fadeRight { + right: 0px; + height: 100%; + position: absolute; + pointer-events: none; + background: linear-gradient(to right, $timelineColor 10%, $light-color); + } + + .divider { + height: 100%; + width: 1px; + position: absolute; + background-color: black; + cursor: col-resize; + pointer-events: none; + } + + .keyframe { + height: 100%; + position: absolute; + } + + .fadeIn-container, + .fadeOut-container, + .body-container { + position: absolute; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + opacity: 0; + } + + +}
\ No newline at end of file diff --git a/src/client/views/animationtimeline/Keyframe.tsx b/src/client/views/animationtimeline/Keyframe.tsx new file mode 100644 index 000000000..bb557289e --- /dev/null +++ b/src/client/views/animationtimeline/Keyframe.tsx @@ -0,0 +1,568 @@ +import * as React from "react"; +import "./Keyframe.scss"; +import "./Timeline.scss"; +import "../globalCssVariables.scss"; +import { observer } from "mobx-react"; +import { observable, reaction, action, IReactionDisposer, observe, computed, runInAction } from "mobx"; +import { Doc, DocListCast, DocListCastAsync } from "../../../new_fields/Doc"; +import { Cast, NumCast } from "../../../new_fields/Types"; +import { List } from "../../../new_fields/List"; +import { createSchema, defaultSpec, makeInterface, listSpec } from "../../../new_fields/Schema"; +import { Transform } from "../../util/Transform"; +import { TimelineMenu } from "./TimelineMenu"; +import { Docs } from "../../documents/Documents"; +import { CollectionDockingView } from "../collections/CollectionDockingView"; +import { undoBatch, UndoManager } from "../../util/UndoManager"; + + + +/** + * Useful static functions that you can use. Mostly for logic, but you can also add UI logic here also + */ +export namespace KeyframeFunc { + + export enum KeyframeType { + end = "end", + fade = "fade", + default = "default", + } + + export enum Direction { + left = "left", + right = "right" + } + + export const findAdjacentRegion = (dir: KeyframeFunc.Direction, currentRegion: Doc, regions: List<Doc>): (RegionData | undefined) => { + let leftMost: (RegionData | undefined) = undefined; + let rightMost: (RegionData | undefined) = undefined; + DocListCast(regions).forEach(region => { + const neighbor = RegionData(region); + if (currentRegion.position! > neighbor.position) { + if (!leftMost || neighbor.position > leftMost.position) { + leftMost = neighbor; + } + } else if (currentRegion.position! < neighbor.position) { + if (!rightMost || neighbor.position < rightMost.position) { + rightMost = neighbor; + } + } + }); + if (dir === Direction.left) { + return leftMost; + } else if (dir === Direction.right) { + return rightMost; + } + }; + + export const calcMinLeft = async (region: Doc, currentBarX: number, ref?: Doc) => { //returns the time of the closet keyframe to the left + let leftKf: (Doc | undefined) = undefined; + let time: number = 0; + const keyframes = await DocListCastAsync(region.keyframes!); + keyframes!.forEach((kf) => { + let compTime = currentBarX; + if (ref) compTime = NumCast(ref.time); + if (NumCast(kf.time) < compTime && NumCast(kf.time) >= time) { + leftKf = kf; + time = NumCast(kf.time); + } + }); + return leftKf; + }; + + + export const calcMinRight = async (region: Doc, currentBarX: number, ref?: Doc) => { //returns the time of the closest keyframe to the right + let rightKf: (Doc | undefined) = undefined; + let time: number = Infinity; + const keyframes = await DocListCastAsync(region.keyframes!); + keyframes!.forEach((kf) => { + let compTime = currentBarX; + if (ref) compTime = NumCast(ref.time); + if (NumCast(kf.time) > compTime && NumCast(kf.time) <= NumCast(time)) { + rightKf = kf; + time = NumCast(kf.time); + } + }); + return rightKf; + }; + + export const defaultKeyframe = () => { + const regiondata = new Doc(); //creating regiondata in MILI + regiondata.duration = 4000; + regiondata.position = 0; + regiondata.fadeIn = 1000; + regiondata.fadeOut = 1000; + regiondata.functions = new List<Doc>(); + regiondata.hasData = false; + return regiondata; + }; + + + export const convertPixelTime = (pos: number, unit: "mili" | "sec" | "min" | "hr", dir: "pixel" | "time", tickSpacing: number, tickIncrement: number) => { + const time = dir === "pixel" ? (pos * tickSpacing) / tickIncrement : (pos / tickSpacing) * tickIncrement; + switch (unit) { + case "mili": + return time; + case "sec": + return dir === "pixel" ? time / 1000 : time * 1000; + case "min": + return dir === "pixel" ? time / 60000 : time * 60000; + case "hr": + return dir === "pixel" ? time / 3600000 : time * 3600000; + default: + return time; + } + }; +} + +export const RegionDataSchema = createSchema({ + position: defaultSpec("number", 0), + duration: defaultSpec("number", 0), + keyframes: listSpec(Doc), + fadeIn: defaultSpec("number", 0), + fadeOut: defaultSpec("number", 0), + functions: listSpec(Doc), + hasData: defaultSpec("boolean", false) +}); +export type RegionData = makeInterface<[typeof RegionDataSchema]>; +export const RegionData = makeInterface(RegionDataSchema); + +interface IProps { + node: Doc; + RegionData: Doc; + collection: Doc; + tickSpacing: number; + tickIncrement: number; + time: number; + changeCurrentBarX: (x: number) => void; + transform: Transform; + makeKeyData: (region:RegionData, pos: number, kftype:KeyframeFunc.KeyframeType) => Doc; +} + + +/** + * + * This class handles the green region stuff + * Key facts: + * + * Structure looks like this + * + * region as a whole + * <------------------------------REGION-------------------------------> + * + * region broken down + * + * <|---------|############ MAIN CONTENT #################|-----------|> .....followed by void......... + * (start) (Fade 2) + * (fade 1) (finish) + * + * + * As you can see, this is different from After Effect and Premiere Pro, but this is how TAG worked. + * If you want to checkout TAG, it's in the lockers, and the password is the usual lab door password. It's the blue laptop. + * If you want to know the exact location of the computer, message me. + * + * @author Andrew Kim + */ +@observer +export class Keyframe extends React.Component<IProps> { + + @observable private _bar = React.createRef<HTMLDivElement>(); + @observable private _mouseToggled = false; + @observable private _doubleClickEnabled = false; + + @computed private get regiondata() { return RegionData(this.regions[this.regions.indexOf(this.props.RegionData)] as Doc); } + @computed private get regions() { return Cast(this.props.node.regions, listSpec(Doc)) as List<Doc>; } + @computed private get keyframes() { return DocListCast(this.regiondata.keyframes); } + @computed private get pixelPosition() { return KeyframeFunc.convertPixelTime(this.regiondata.position, "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement); } + @computed private get pixelDuration() { return KeyframeFunc.convertPixelTime(this.regiondata.duration, "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement); } + @computed private get pixelFadeIn() { return KeyframeFunc.convertPixelTime(this.regiondata.fadeIn, "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement); } + @computed private get pixelFadeOut() { return KeyframeFunc.convertPixelTime(this.regiondata.fadeOut, "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement); } + + componentDidMount() { + setTimeout(() => { //giving it a temporary 1sec delay... + if (!this.regiondata.keyframes) this.regiondata.keyframes = new List<Doc>(); + const start = this.props.makeKeyData(this.regiondata, this.regiondata.position, KeyframeFunc.KeyframeType.end); + const fadeIn = this.props.makeKeyData(this.regiondata, this.regiondata.position + this.regiondata.fadeIn, KeyframeFunc.KeyframeType.fade); + const fadeOut = this.props.makeKeyData(this.regiondata, this.regiondata.position + this.regiondata.duration - this.regiondata.fadeOut, KeyframeFunc.KeyframeType.fade); + const finish = this.props.makeKeyData(this.regiondata, this.regiondata.position + this.regiondata.duration,KeyframeFunc.KeyframeType.end); + (fadeIn.key as Doc).opacity = 1; + (fadeOut.key as Doc).opacity = 1; + (start.key as Doc).opacity = 0.1; + (finish.key as Doc).opacity = 0.1; + this.forceUpdate(); //not needed, if setTimeout is gone... + }, 1000); + } + + + + + @action + onBarPointerDown = (e: React.PointerEvent) => { + e.preventDefault(); + e.stopPropagation(); + const clientX = e.clientX; + if (this._doubleClickEnabled) { + this.createKeyframe(clientX); + this._doubleClickEnabled = false; + } else { + setTimeout(() => { + if (!this._mouseToggled && this._doubleClickEnabled) this.props.changeCurrentBarX(this.pixelPosition + (clientX - this._bar.current!.getBoundingClientRect().left) * this.props.transform.Scale); + this._mouseToggled = false; + this._doubleClickEnabled = false; + }, 200); + this._doubleClickEnabled = true; + document.addEventListener("pointermove", this.onBarPointerMove); + document.addEventListener("pointerup", (e: PointerEvent) => { + document.removeEventListener("pointermove", this.onBarPointerMove); + }); + } + } + + @action + onBarPointerMove = (e: PointerEvent) => { + e.preventDefault(); + e.stopPropagation(); + if (e.movementX !== 0) { + this._mouseToggled = true; + } + const left = KeyframeFunc.findAdjacentRegion(KeyframeFunc.Direction.left, this.regiondata, this.regions)!; + const right = KeyframeFunc.findAdjacentRegion(KeyframeFunc.Direction.right, this.regiondata, this.regions!); + const prevX = this.regiondata.position; + const futureX = this.regiondata.position + KeyframeFunc.convertPixelTime(e.movementX, "mili", "time", this.props.tickSpacing, this.props.tickIncrement); + if (futureX <= 0) { + this.regiondata.position = 0; + } else if ((left && left.position + left.duration >= futureX)) { + this.regiondata.position = left.position + left.duration; + } else if ((right && right.position <= futureX + this.regiondata.duration)) { + this.regiondata.position = right.position - this.regiondata.duration; + } else { + this.regiondata.position = futureX; + } + const movement = this.regiondata.position - prevX; + this.keyframes.forEach(kf => { + kf.time = NumCast(kf.time) + movement; + }); + } + + @action + onResizeLeft = (e: React.PointerEvent) => { + e.preventDefault(); + e.stopPropagation(); + document.addEventListener("pointermove", this.onDragResizeLeft); + document.addEventListener("pointerup", () => { + document.removeEventListener("pointermove", this.onDragResizeLeft); + }); + } + + @action + onResizeRight = (e: React.PointerEvent) => { + e.preventDefault(); + e.stopPropagation(); + document.addEventListener("pointermove", this.onDragResizeRight); + document.addEventListener("pointerup", () => { + document.removeEventListener("pointermove", this.onDragResizeRight); + }); + } + + @action + onDragResizeLeft = (e: PointerEvent) => { + e.preventDefault(); + e.stopPropagation(); + const bar = this._bar.current!; + const offset = KeyframeFunc.convertPixelTime(Math.round((e.clientX - bar.getBoundingClientRect().left) * this.props.transform.Scale), "mili", "time", this.props.tickSpacing, this.props.tickIncrement); + const leftRegion = KeyframeFunc.findAdjacentRegion(KeyframeFunc.Direction.left, this.regiondata, this.regions); + if (leftRegion && this.regiondata.position + offset <= leftRegion.position + leftRegion.duration) { + this.regiondata.position = leftRegion.position + leftRegion.duration; + this.regiondata.duration = NumCast(this.keyframes[this.keyframes.length - 1].time) - (leftRegion.position + leftRegion.duration); + } else if (NumCast(this.keyframes[1].time) + offset >= NumCast(this.keyframes[2].time)) { + this.regiondata.position = NumCast(this.keyframes[2].time) - this.regiondata.fadeIn; + this.regiondata.duration = NumCast(this.keyframes[this.keyframes.length - 1].time) - NumCast(this.keyframes[2].time) + this.regiondata.fadeIn; + } else if (NumCast(this.keyframes[0].time) + offset <= 0) { + this.regiondata.position = 0; + this.regiondata.duration = NumCast(this.keyframes[this.keyframes.length - 1].time); + } else { + this.regiondata.duration -= offset; + this.regiondata.position += offset; + } + this.keyframes[0].time = this.regiondata.position; + this.keyframes[1].time = this.regiondata.position + this.regiondata.fadeIn; + } + + + @action + onDragResizeRight = (e: PointerEvent) => { + e.preventDefault(); + e.stopPropagation(); + const bar = this._bar.current!; + const offset = KeyframeFunc.convertPixelTime(Math.round((e.clientX - bar.getBoundingClientRect().right) * this.props.transform.Scale), "mili", "time", this.props.tickSpacing, this.props.tickIncrement); + const rightRegion = KeyframeFunc.findAdjacentRegion(KeyframeFunc.Direction.right, this.regiondata, this.regions); + const fadeOutKeyframeTime = NumCast(this.keyframes[this.keyframes.length - 3].time); + if (this.regiondata.position + this.regiondata.duration - this.regiondata.fadeOut + offset <= fadeOutKeyframeTime) { //case 1: when third to last keyframe is in the way + this.regiondata.duration = fadeOutKeyframeTime - this.regiondata.position + this.regiondata.fadeOut; + } else if (rightRegion && (this.regiondata.position + this.regiondata.duration + offset >= rightRegion.position)) { + this.regiondata.duration = rightRegion.position - this.regiondata.position; + } else { + this.regiondata.duration += offset; + } + this.keyframes[this.keyframes.length - 2].time = this.regiondata.position + this.regiondata.duration - this.regiondata.fadeOut; + this.keyframes[this.keyframes.length - 1].time = this.regiondata.position + this.regiondata.duration; + } + + + @action + createKeyframe = async (clientX: number) => { + this._mouseToggled = true; + const bar = this._bar.current!; + const offset = KeyframeFunc.convertPixelTime(Math.round((clientX - bar.getBoundingClientRect().left) * this.props.transform.Scale), "mili", "time", this.props.tickSpacing, this.props.tickIncrement); + if (offset > this.regiondata.fadeIn && offset < this.regiondata.duration - this.regiondata.fadeOut) { //make sure keyframe is not created inbetween fades and ends + const position = this.regiondata.position; + this.props.makeKeyData(this.regiondata, Math.round(position + offset), KeyframeFunc.KeyframeType.default); + this.regiondata.hasData = true; + this.props.changeCurrentBarX(KeyframeFunc.convertPixelTime(Math.round(position + offset), "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement)); //first move the keyframe to the correct location and make a copy so the correct file gets coppied + + } + } + + + @action + moveKeyframe = async (e: React.MouseEvent, kf: Doc) => { + e.preventDefault(); + e.stopPropagation(); + this.props.changeCurrentBarX(KeyframeFunc.convertPixelTime(NumCast(kf.time!), "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement)); + } + + /** + * custom keyframe context menu items (when clicking on the keyframe circle) + */ + @action + makeKeyframeMenu = (kf: Doc, e: MouseEvent) => { + TimelineMenu.Instance.addItem("button", "Show Data", () => { + runInAction(() => { + const kvp = Docs.Create.KVPDocument(Cast(kf.key, Doc) as Doc, { _width: 300, _height: 300 }); + CollectionDockingView.AddRightSplit(kvp, (kf.key as Doc).data as Doc); + }); + }), + TimelineMenu.Instance.addItem("button", "Delete", () => { + runInAction(() => { + (this.regiondata.keyframes as List<Doc>).splice(this.keyframes.indexOf(kf), 1); + this.forceUpdate(); + }); + }), + TimelineMenu.Instance.addItem("input", "Move", (val) => { + runInAction(() => { + let cannotMove: boolean = false; + const kfIndex: number = this.keyframes.indexOf(kf); + if (val < 0 || (val < NumCast(this.keyframes[kfIndex - 1].time) || val > NumCast(this.keyframes[kfIndex + 1].time))) { + cannotMove = true; + } + if (!cannotMove) { + this.keyframes[kfIndex].time = parseInt(val, 10); + this.keyframes[1].time = this.regiondata.position + this.regiondata.fadeIn; + } + }); + }); + TimelineMenu.Instance.addMenu("Keyframe"); + TimelineMenu.Instance.openMenu(e.clientX, e.clientY); + } + + /** + * context menu for region (anywhere on the green region). + */ + @action + makeRegionMenu = (kf: Doc, e: MouseEvent) => { + TimelineMenu.Instance.addItem("button", "Remove Region", () => { + runInAction(() => { + this.regions.splice(this.regions.indexOf(this.props.RegionData), 1); + } + ); + }), + TimelineMenu.Instance.addItem("input", `fadeIn: ${this.regiondata.fadeIn}ms`, (val) => { + runInAction(() => { + let cannotMove: boolean = false; + if (val < 0 || val > NumCast(this.keyframes[2].time) - this.regiondata.position) { + cannotMove = true; + } + if (!cannotMove) { + this.regiondata.fadeIn = parseInt(val, 10); + this.keyframes[1].time = this.regiondata.position + this.regiondata.fadeIn; + } + }); + }), + TimelineMenu.Instance.addItem("input", `fadeOut: ${this.regiondata.fadeOut}ms`, (val) => { + runInAction(() => { + let cannotMove: boolean = false; + if (val < 0 || val > this.regiondata.position + this.regiondata.duration - NumCast(this.keyframes[this.keyframes.length - 3].time)) { + cannotMove = true; + } + if (!cannotMove) { + this.regiondata.fadeOut = parseInt(val, 10); + this.keyframes[this.keyframes.length - 2].time = this.regiondata.position + this.regiondata.duration - val; + } + }); + }), + TimelineMenu.Instance.addItem("input", `position: ${this.regiondata.position}ms`, (val) => { + runInAction(() => { + const prevPosition = this.regiondata.position; + let cannotMove: boolean = false; + DocListCast(this.regions).forEach(region => { + if (NumCast(region.position) !== this.regiondata.position) { + if ((val < 0) || (val > NumCast(region.position) && val < NumCast(region.position) + NumCast(region.duration) || (this.regiondata.duration + val > NumCast(region.position) && this.regiondata.duration + val < NumCast(region.position) + NumCast(region.duration)))) { + cannotMove = true; + } + } + }); + if (!cannotMove) { + this.regiondata.position = parseInt(val, 10); + this.updateKeyframes(this.regiondata.position - prevPosition); + } + }); + }), + TimelineMenu.Instance.addItem("input", `duration: ${this.regiondata.duration}ms`, (val) => { + runInAction(() => { + let cannotMove: boolean = false; + DocListCast(this.regions).forEach(region => { + if (NumCast(region.position) !== this.regiondata.position) { + val += this.regiondata.position; + if ((val < 0) || (val > NumCast(region.position) && val < NumCast(region.position) + NumCast(region.duration))) { + cannotMove = true; + } + } + }); + if (!cannotMove) { + this.regiondata.duration = parseInt(val, 10); + this.keyframes[this.keyframes.length - 1].time = this.regiondata.position + this.regiondata.duration; + this.keyframes[this.keyframes.length - 2].time = this.regiondata.position + this.regiondata.duration - this.regiondata.fadeOut; + } + }); + }), + TimelineMenu.Instance.addMenu("Region"); + TimelineMenu.Instance.openMenu(e.clientX, e.clientY); + } + + @action + updateKeyframes = (incr: number, filter: number[] = []) => { + this.keyframes.forEach(kf => { + if (!filter.includes(this.keyframes.indexOf(kf))) { + kf.time = NumCast(kf.time) + incr; + } + }); + } + + /** + * hovering effect when hovered (hidden div darkens) + */ + @action + onContainerOver = (e: React.PointerEvent, ref: React.RefObject<HTMLDivElement>) => { + e.preventDefault(); + e.stopPropagation(); + const div = ref.current!; + div.style.opacity = "1"; + Doc.BrushDoc(this.props.node); + } + + /** + * hovering effect when hovered out (hidden div becomes invisible) + */ + @action + onContainerOut = (e: React.PointerEvent, ref: React.RefObject<HTMLDivElement>) => { + e.preventDefault(); + e.stopPropagation(); + const div = ref.current!; + div.style.opacity = "0"; + Doc.UnBrushDoc(this.props.node); + } + + + ///////////////////////UI STUFF ///////////////////////// + + + /** + * drawing keyframe. Handles both keyframe with a circle (one that you create by double clicking) and one without circle (fades) + * this probably needs biggest change, since everyone expected all keyframes to have a circle (and draggable) + */ + @action + drawKeyframes = () => { + const keyframeDivs: JSX.Element[] = []; + DocListCast(this.regiondata.keyframes).forEach(kf => { + if (kf.type as KeyframeFunc.KeyframeType !== KeyframeFunc.KeyframeType.end) { + keyframeDivs.push( + <><div className="keyframe" style={{ left: `${KeyframeFunc.convertPixelTime(NumCast(kf.time), "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement) - this.pixelPosition}px` }}> + <div className="divider"></div> + <div className="keyframeCircle keyframe-indicator" onPointerDown={(e) => { e.preventDefault(); e.stopPropagation(); this.moveKeyframe(e, kf); }} onContextMenu={(e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + this.makeKeyframeMenu(kf, e.nativeEvent); + }} onDoubleClick={(e) => { e.preventDefault(); e.stopPropagation(); }}></div> + + </div> + <div className="keyframe-information"></div> + </> + + ); + } else { + keyframeDivs.push( + <div className="keyframe" style={{ left: `${KeyframeFunc.convertPixelTime(NumCast(kf.time), "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement) - this.pixelPosition}px` }}> + <div className="divider"></div> + </div> + ); + } + }); + return keyframeDivs; + } + + /** + * drawing the hidden divs that partition different intervals within a region. + */ + @action + drawKeyframeDividers = () => { + const keyframeDividers: JSX.Element[] = []; + DocListCast(this.regiondata.keyframes).forEach(kf => { + const index = this.keyframes.indexOf(kf); + if (index !== this.keyframes.length - 1) { + const right = this.keyframes[index + 1]; + const bodyRef = React.createRef<HTMLDivElement>(); + const kfPos = KeyframeFunc.convertPixelTime(NumCast(kf.time), "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement); + const rightPos = KeyframeFunc.convertPixelTime(NumCast(right.time), "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement); + keyframeDividers.push( + <div ref={bodyRef} className="body-container" style={{ left: `${kfPos - this.pixelPosition}px`, width: `${rightPos - kfPos}px` }} + onPointerOver={(e) => { e.preventDefault(); e.stopPropagation(); this.onContainerOver(e, bodyRef); }} + onPointerOut={(e) => { e.preventDefault(); e.stopPropagation(); this.onContainerOut(e, bodyRef); }} + onContextMenu={(e) => { + e.preventDefault(); + e.stopPropagation(); + if (index !== 0 || index !== this.keyframes.length - 2) { + this._mouseToggled = true; + } + this.makeRegionMenu(kf, e.nativeEvent); + }}> + </div> + ); + } + }); + return keyframeDividers; + } + + /** + * rendering that green region + */ + //154, 206, 223 + render() { + return ( + <div> + <div className="bar" ref={this._bar} style={{ + transform: `translate(${this.pixelPosition}px)`, + width: `${this.pixelDuration}px`, + background: `linear-gradient(90deg, rgba(154, 206, 223, 0) 0%, rgba(154, 206, 223, 1) ${this.pixelFadeIn / this.pixelDuration * 100}%, rgba(154, 206, 223, 1) ${(this.pixelDuration - this.pixelFadeOut) / this.pixelDuration * 100}%, rgba(154, 206, 223, 0) 100% )` + }} + onPointerDown={this.onBarPointerDown + }> + <div className="leftResize keyframe-indicator" onPointerDown={this.onResizeLeft} ></div> + {/* <div className="keyframe-information"></div> */} + <div className="rightResize keyframe-indicator" onPointerDown={this.onResizeRight}></div> + {/* <div className="keyframe-information"></div> */} + {this.drawKeyframes()} + {this.drawKeyframeDividers()} + </div> + </div> + ); + } +}
\ No newline at end of file diff --git a/src/client/views/animationtimeline/Timeline.scss b/src/client/views/animationtimeline/Timeline.scss new file mode 100644 index 000000000..f90249771 --- /dev/null +++ b/src/client/views/animationtimeline/Timeline.scss @@ -0,0 +1,322 @@ +@import "./../globalCssVariables.scss"; + +$timelineColor: #9acedf; +$timelineDark: #77a1aa; + +.timeline-toolbox { + position: absolute; + margin: 0px; + padding: 0px; + display: flex; + align-items: flex-start; + flex-direction: row; + // justify-content: space-evenly; + align-items: center; + top: 3px; + width: 100%; + + .overview-tool { + display: flex; + justify-content: center; + } + + .playbackControls { + display: flex; + margin-left: 30px; + max-width: 84px; + width: 84px; + + .timeline-icon { + color: $timelineColor; + margin-left: 3px; + } + + } + + .grid-box { + display: flex; + // grid-template-columns: [first] 20% [line2] 20% [line3] 60%; + width: calc(100% - 150px); + // width: 100%; + margin-left: 10px; + + .time-box { + margin-left: 10px; + min-width: 140px; + display: flex; + justify-content: center; + align-items: center; + + .resetView-tool { + width: 30px; + height: 30px; + display: flex; + justify-content: center; + align-items: center; + margin: 3px; + color: $timelineDark; + } + + .resetView-tool:hover { + -webkit-transform: scale(1.1); + -ms-transform: scale(1.1); + transform: scale(1.1); + transition: .2s ease; + } + } + + .mode-box { + display: flex; + margin-left: 5px; + } + + .overview-box { + width: 80%; + display: flex; + } + + div { + padding: 0px; + // margin-left: 10px; + } + } + + .overview-tool { + display: flex; + justify-content: center; + align-items: center; + } + + .animation-text { + // font-size: 16px; + height: auto; + width: auto; + white-space: nowrap; + font-size: 14px; + color: black; + letter-spacing: 2px; + text-transform: uppercase; + } + + .round-toggle { + height: 20px; + width: 40px; + min-width: 40px; + background-color: white; + border: 2px solid $timelineDark; + border-radius: 20px; + animation-fill-mode: forwards; + animation-duration: 500ms; + top: 30px; + margin-left: 5px; + + input { + position: absolute; + opacity: 0; + height: 0; + width: 0; + } + + .round-toggle-slider { + height: 17px; + width: 17px; + background-color: white; + border: 2px solid $timelineDark; + border-radius: 50%; + transition: transform 500ms ease-in-out; + margin-left: 0px; + // margin-top: 0.5px; + } + } + +} + +.time-input { + height: 20px; + // width: 120px; + width: 100%; + white-space: nowrap; + font-size: 12px; + color: black; + letter-spacing: 2px; + text-transform: uppercase; + padding-left: 5px; + margin-left: 5px; +} + +.tick { + height: 100%; + width: 2px; + background-color: black; + color: black; +} + +.number-label { + color: black; + transform: rotate(-90deg) translate(-15px, 8px); + font-size: .85em; +} + +.timeline-container { + width: 100%; + height: 300px; + position: absolute; + background-color: $light-color-secondary; + border-bottom: 2px solid $timelineDark; + transition: transform 500ms ease; + + .info-container { + margin-top: 50px; + right: 20px; + position: absolute; + height: calc(100% - 100px); + width: calc(100% - 140px); + overflow: hidden; + + .scrubberbox { + position: absolute; + background-color: transparent; + height: 30px; + width: 100%; + + } + + .scrubber { + top: 30px; + height: 100%; + width: 2px; + position: absolute; + z-index: 1001; + background-color: black; + pointer-events: none; + + .scrubberhead { + top: -20px; + height: 20px; + width: 20px; + background-color: white; + border-radius: 50%; + border: 3px solid black; + left: -9px; + position: absolute; + pointer-events: all; + } + } + + .trackbox { + top: 30px; + height: calc(100% - 30px); + // height: 100%; + // height: 100%; + width: 100%; + border-top: 2px solid black; + border-bottom: 2px solid black; + overflow: hidden; + // overflow-y: scroll; + background-color: white; + position: absolute; + // box-shadow: -10px 0px 10px 10px red; + } + + } + + .currentTime { + // background: red; + font-size: 12px; + display: flex; + justify-content: center; + align-items: center; + height: 40px; + top: 40px; + position: relative; + width: 100px; + margin-left: 20px; + } + + .title-container { + // margin-top: 80px; + margin-top: 40px; + margin-left: 20px; + height: calc(100% - 100px - 30px); + // height: 100%; + width: 100px; + background-color: white; + overflow: hidden; + border-left: 2px solid black; + border-top: 2px solid black; + border-bottom: 2px solid black; + border-right: 2px solid $timelineDark; + + .datapane { + top: 0px; + width: 100px; + height: 30%; + border: 1px solid $dark-color; + font-size: 12px; + line-height: 11px; + background-color: $timelineDark; + color: white; + position: relative; + float: left; + padding: 3px; + border-style: solid; + overflow-y: scroll; + overflow-x: hidden; + + p { + hyphens: auto; + } + + } + } + + .resize { + bottom: 0px; + position: absolute; + height: 20px; + width: 40px; + left: calc(50% - 25px); + color: $timelineDark; + } +} + + + +.overview { + position: absolute; + height: 50px; + width: 200px; + background-color: black; + + .container { + position: absolute; + float: left 0px; + top: 25%; + height: 75%; + width: 100%; + background-color: grey; + } +} + + +.timeline-checker { + height: auto; + width: auto; + overflow: hidden; + position: absolute; + display: flex; + padding: 10px 10px; + + div { + height: auto; + width: auto; + overflow: hidden; + margin: 0px 10px; + cursor: pointer + } + + .check { + width: 50px; + height: 50px; + } +}
\ No newline at end of file diff --git a/src/client/views/animationtimeline/Timeline.tsx b/src/client/views/animationtimeline/Timeline.tsx new file mode 100644 index 000000000..e9caa2b2a --- /dev/null +++ b/src/client/views/animationtimeline/Timeline.tsx @@ -0,0 +1,632 @@ +import * as React from "react"; +import "./Timeline.scss"; +import { listSpec } from "../../../new_fields/Schema"; +import { observer } from "mobx-react"; +import { Track } from "./Track"; +import { observable, action, computed, runInAction, IReactionDisposer, reaction } from "mobx"; +import { Cast, NumCast, StrCast, BoolCast } from "../../../new_fields/Types"; +import { List } from "../../../new_fields/List"; +import { Doc, DocListCast } from "../../../new_fields/Doc"; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faPlayCircle, faBackward, faForward, faGripLines, faPauseCircle, faEyeSlash, faEye, faCheckCircle, faTimesCircle } from "@fortawesome/free-solid-svg-icons"; +import { ContextMenu } from "../ContextMenu"; +import { TimelineOverview } from "./TimelineOverview"; +import { FieldViewProps } from "../nodes/FieldView"; +import { KeyframeFunc } from "./Keyframe"; +import { Utils } from "../../../Utils"; +import { createPromiseCapability } from "../../../../deploy/assets/pdf.worker"; + + +/** + * Timeline class controls most of timeline functions besides individual keyframe and track mechanism. Main functions are + * zooming, panning, currentBarX (scrubber movement). Most of the UI stuff is also handled here. You shouldn't really make + * any logical changes here. Most work is needed on UI. + * + * The hierarchy works this way: + * + * Timeline.tsx --> Track.tsx --> Keyframe.tsx + | | + | TimelineMenu.tsx (timeline's custom contextmenu) + | + | + TimelineOverview.tsx (youtube like dragging thing is play mode, complex dragging thing in editing mode) + + + Most style changes are in SCSS file. + If you have any questions, email me or text me. + @author Andrew Kim + */ + + +@observer +export class Timeline extends React.Component<FieldViewProps> { + + + //readonly constants + private readonly DEFAULT_TICK_SPACING: number = 50; + private readonly MAX_TITLE_HEIGHT = 75; + private readonly MAX_CONTAINER_HEIGHT: number = 800; + private readonly DEFAULT_TICK_INCREMENT: number = 1000; + + //height variables + private DEFAULT_CONTAINER_HEIGHT: number = 330; + private MIN_CONTAINER_HEIGHT: number = 205; + + //react refs + @observable private _trackbox = React.createRef<HTMLDivElement>(); + @observable private _titleContainer = React.createRef<HTMLDivElement>(); + @observable private _timelineContainer = React.createRef<HTMLDivElement>(); + @observable private _infoContainer = React.createRef<HTMLDivElement>(); + @observable private _roundToggleRef = React.createRef<HTMLDivElement>(); + @observable private _roundToggleContainerRef = React.createRef<HTMLDivElement>(); + @observable private _timeInputRef = React.createRef<HTMLInputElement>(); + + //boolean vars and instance vars + @observable private _currentBarX: number = 0; + @observable private _windSpeed: number = 1; + @observable private _isPlaying: boolean = false; //scrubber playing + @observable private _totalLength: number = 0; + @observable private _visibleLength: number = 0; + @observable private _visibleStart: number = 0; + @observable private _containerHeight: number = this.DEFAULT_CONTAINER_HEIGHT; + @observable private _tickSpacing = this.DEFAULT_TICK_SPACING; + @observable private _tickIncrement = this.DEFAULT_TICK_INCREMENT; + @observable private _time = 100000; //DEFAULT + @observable private _playButton = faPlayCircle; + @observable private _timelineVisible = false; + @observable private _mouseToggled = false; + @observable private _doubleClickEnabled = false; + @observable private _titleHeight = 0; + + // so a reaction can be made + @observable public _isAuthoring = this.props.Document.isATOn; + @observable private _panelWidth = 0; + + /** + * collection get method. Basically defines what defines collection's children. These will be tracked in the timeline. Do not edit. + */ + @computed + private get children(): List<Doc> { + const extendedDocument = ["image", "video", "pdf"].includes(StrCast(this.props.Document.type)); + if (extendedDocument) { + if (this.props.Document.data_ext) { + return Cast((Cast(this.props.Document.data_ext, Doc) as Doc).annotations, listSpec(Doc)) as List<Doc>; + } else { + return new List<Doc>(); + } + } + return Cast(this.props.Document[this.props.fieldKey], listSpec(Doc)) as List<Doc>; + } + + /////////lifecycle functions//////////// + componentWillMount() { + const relativeHeight = window.innerHeight / 20; //sets height to arbitrary size, relative to innerHeight + this._titleHeight = relativeHeight < this.MAX_TITLE_HEIGHT ? relativeHeight : this.MAX_TITLE_HEIGHT; //check if relHeight is less than Maxheight. Else, just set relheight to max + this.MIN_CONTAINER_HEIGHT = this._titleHeight + 130; //offset + this.DEFAULT_CONTAINER_HEIGHT = this._titleHeight * 2 + 130; //twice the titleheight + offset + } + + componentDidMount() { + runInAction(() => { + if (!this.props.Document.AnimationLength) { //if animation length did not exist + this.props.Document.AnimationLength = this._time; //set it to default time + } else { + this._time = NumCast(this.props.Document.AnimationLength); //else, set time to animationlength stored from before + } + this._totalLength = this._tickSpacing * (this._time / this._tickIncrement); //the entire length of the timeline div (actual div part itself) + this._visibleLength = this._infoContainer.current!.getBoundingClientRect().width; //the visible length of the timeline (the length that you current see) + this._visibleStart = this._infoContainer.current!.scrollLeft; //where the div starts + this.props.Document.isATOn = !this.props.Document.isATOn; //turns the boolean on, saying AT (animation timeline) is on + this.toggleHandle(); + }); + } + + componentWillUnmount() { + runInAction(() => { + this.props.Document.AnimationLength = this._time; //save animation length + }); + } + ///////////////////////////////////////////////// + + /** + * React Functional Component + * Purpose: For drawing Tick marks across the timeline in authoring mode + */ + @action + drawTicks = () => { + const ticks = []; + for (let i = 0; i < this._time / this._tickIncrement; i++) { + ticks.push(<div key={Utils.GenerateGuid()} className="tick" style={{ transform: `translate(${i * this._tickSpacing}px)`, position: "absolute", pointerEvents: "none" }}> <p className="number-label">{this.toReadTime(i * this._tickIncrement)}</p></div>); + } + return ticks; + } + + /** + * changes the scrubber to actual pixel position + */ + @action + changeCurrentBarX = (pixel: number) => { + pixel <= 0 ? this._currentBarX = 0 : pixel >= this._totalLength ? this._currentBarX = this._totalLength : this._currentBarX = pixel; + } + + //for playing + @action + onPlay = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + this.play(); + } + + /** + * when playbutton is clicked + */ + @action + play = () => { + if (this._isPlaying) { + this._isPlaying = false; + this._playButton = faPlayCircle; + } else { + this._isPlaying = true; + this._playButton = faPauseCircle; + const playTimeline = () => { + if (this._isPlaying) { + if (this._currentBarX >= this._totalLength) { + this.changeCurrentBarX(0); + } else { + this.changeCurrentBarX(this._currentBarX + this._windSpeed); + } + setTimeout(playTimeline, 15); + } + }; + playTimeline(); + } + } + + + /** + * fast forward the timeline scrubbing + */ + @action + windForward = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + if (this._windSpeed < 64) { //max speed is 32 + this._windSpeed = this._windSpeed * 2; + } + } + + /** + * rewind the timeline scrubbing + */ + @action + windBackward = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + if (this._windSpeed > 1 / 16) { // min speed is 1/8 + this._windSpeed = this._windSpeed / 2; + } + } + + /** + * scrubber down + */ + @action + onScrubberDown = (e: React.PointerEvent) => { + e.preventDefault(); + e.stopPropagation(); + document.addEventListener("pointermove", this.onScrubberMove); + document.addEventListener("pointerup", () => { + document.removeEventListener("pointermove", this.onScrubberMove); + }); + } + + /** + * when there is any scrubber movement + */ + @action + onScrubberMove = (e: PointerEvent) => { + e.preventDefault(); + e.stopPropagation(); + const scrubberbox = this._infoContainer.current!; + const left = scrubberbox.getBoundingClientRect().left; + const offsetX = Math.round(e.clientX - left) * this.props.ScreenToLocalTransform().Scale; + this.changeCurrentBarX(offsetX + this._visibleStart); //changes scrubber to clicked scrubber position + } + + /** + * when panning the timeline (in editing mode) + */ + @action + onPanDown = (e: React.PointerEvent) => { + e.preventDefault(); + e.stopPropagation(); + const clientX = e.clientX; + if (this._doubleClickEnabled) { + this._doubleClickEnabled = false; + } else { + setTimeout(() => { + if (!this._mouseToggled && this._doubleClickEnabled) this.changeCurrentBarX(this._trackbox.current!.scrollLeft + clientX - this._trackbox.current!.getBoundingClientRect().left); + this._mouseToggled = false; + this._doubleClickEnabled = false; + }, 200); + this._doubleClickEnabled = true; + document.addEventListener("pointermove", this.onPanMove); + document.addEventListener("pointerup", () => { + document.removeEventListener("pointermove", this.onPanMove); + if (!this._doubleClickEnabled) { + this._mouseToggled = false; + } + }); + + } + } + + /** + * when moving the timeline (in editing mode) + */ + @action + onPanMove = (e: PointerEvent) => { + e.preventDefault(); + e.stopPropagation(); + if (e.movementX !== 0 || e.movementY !== 0) { + this._mouseToggled = true; + } + const trackbox = this._trackbox.current!; + const titleContainer = this._titleContainer.current!; + this.movePanX(this._visibleStart - e.movementX); + trackbox.scrollTop = trackbox.scrollTop - e.movementY; + titleContainer.scrollTop = titleContainer.scrollTop - e.movementY; + if (this._visibleStart + this._visibleLength + 20 >= this._totalLength) { + this._visibleStart -= e.movementX; + this._totalLength -= e.movementX; + this._time -= KeyframeFunc.convertPixelTime(e.movementX, "mili", "time", this._tickSpacing, this._tickIncrement); + this.props.Document.AnimationLength = this._time; + } + + } + + + @action + movePanX = (pixel: number) => { + const infoContainer = this._infoContainer.current!; + infoContainer.scrollLeft = pixel; + this._visibleStart = infoContainer.scrollLeft; + } + + /** + * resizing timeline (in editing mode) (the hamburger drag icon) + */ + @action + onResizeDown = (e: React.PointerEvent) => { + e.preventDefault(); + e.stopPropagation(); + document.addEventListener("pointermove", this.onResizeMove); + document.addEventListener("pointerup", () => { + document.removeEventListener("pointermove", this.onResizeMove); + }); + } + + @action + onResizeMove = (e: PointerEvent) => { + e.preventDefault(); + e.stopPropagation(); + const offset = e.clientY - this._timelineContainer.current!.getBoundingClientRect().bottom; + // let offset = 0; + if (this._containerHeight + offset <= this.MIN_CONTAINER_HEIGHT) { + this._containerHeight = this.MIN_CONTAINER_HEIGHT; + } else if (this._containerHeight + offset >= this.MAX_CONTAINER_HEIGHT) { + this._containerHeight = this.MAX_CONTAINER_HEIGHT; + } else { + this._containerHeight += offset; + } + } + + /** + * for displaying time to standard min:sec + */ + @action + toReadTime = (time: number): string => { + time = time / 1000; + const inSeconds = Math.round(time * 100) / 100; + + const min: (string | number) = Math.floor(inSeconds / 60); + const sec: (string | number) = (Math.round((inSeconds % 60) * 100) / 100); + let secString = sec.toFixed(2); + + if (Math.floor(sec / 10) === 0) { + secString = "0" + secString; + } + + return `${min}:${secString}`; + } + + + /** + * context menu function. + * opens the timeline or closes the timeline. + * Used in: Freeform + */ + timelineContextMenu = (e: React.MouseEvent): void => { + ContextMenu.Instance.addItem({ + description: (this._timelineVisible ? "Close" : "Open") + " Animation Timeline", event: action(() => { + this._timelineVisible = !this._timelineVisible; + }), icon: this._timelineVisible ? faEyeSlash : faEye + }); + } + + + /** + * timeline zoom function + * use mouse middle button to zoom in/out the timeline + */ + @action + onWheelZoom = (e: React.WheelEvent) => { + e.preventDefault(); + e.stopPropagation(); + const offset = e.clientX - this._infoContainer.current!.getBoundingClientRect().left; + const prevTime = KeyframeFunc.convertPixelTime(this._visibleStart + offset, "mili", "time", this._tickSpacing, this._tickIncrement); + const prevCurrent = KeyframeFunc.convertPixelTime(this._currentBarX, "mili", "time", this._tickSpacing, this._tickIncrement); + e.deltaY < 0 ? this.zoom(true) : this.zoom(false); + const currPixel = KeyframeFunc.convertPixelTime(prevTime, "mili", "pixel", this._tickSpacing, this._tickIncrement); + const currCurrent = KeyframeFunc.convertPixelTime(prevCurrent, "mili", "pixel", this._tickSpacing, this._tickIncrement); + this._infoContainer.current!.scrollLeft = currPixel - offset; + this._visibleStart = currPixel - offset > 0 ? currPixel - offset : 0; + this._visibleStart += this._visibleLength + this._visibleStart > this._totalLength ? this._totalLength - (this._visibleStart + this._visibleLength) : 0; + this.changeCurrentBarX(currCurrent); + } + + /** + * zooming mechanism (increment and spacing changes) + */ + @action + zoom = (dir: boolean) => { + let spacingChange = this._tickSpacing; + let incrementChange = this._tickIncrement; + if (dir) { + if (!(this._tickSpacing === 100 && this._tickIncrement === 1000)) { + if (this._tickSpacing >= 100) { + incrementChange /= 2; + spacingChange = 50; + } else { + spacingChange += 5; + } + } + } else { + if (this._tickSpacing <= 50) { + spacingChange = 100; + incrementChange *= 2; + } else { + spacingChange -= 5; + } + } + const finalLength = spacingChange * (this._time / incrementChange); + if (finalLength >= this._infoContainer.current!.getBoundingClientRect().width) { + this._totalLength = finalLength; + this._tickSpacing = spacingChange; + this._tickIncrement = incrementChange; + } + } + + /** + * tool box includes the toggle buttons at the top of the timeline (both editing mode and play mode) + */ + private timelineToolBox = (scale: number, totalTime: number) => { + const size = 40 * scale; //50 is default + const iconSize = 25; + + //decides if information should be omitted because the timeline is very small + // if its less than 950 pixels then it's going to be overlapping + let shouldCompress = false; + const width: number = this.props.PanelWidth(); + if (width < 850) { + shouldCompress = true; + } + + let modeString, overviewString, lengthString; + const modeType = this.props.Document.isATOn ? "Author" : "Play"; + + if (!shouldCompress) { + modeString = "Mode: " + modeType; + overviewString = "Overview:"; + lengthString = "Length: "; + } + else { + modeString = modeType; + overviewString = ""; + lengthString = ""; + } + + // let rightInfo = this.timeIndicator; + + return ( + <div key="timeline_toolbox" className="timeline-toolbox" style={{ height: `${size}px` }}> + <div className="playbackControls"> + <div className="timeline-icon" key="timeline_windBack" onClick={this.windBackward} title="Slow Down Animation"> <FontAwesomeIcon icon={faBackward} style={{ height: `${iconSize}px`, width: `${iconSize}px` }} /> </div> + <div className="timeline-icon" key=" timeline_play" onClick={this.onPlay} title="Play/Pause"> <FontAwesomeIcon icon={this._playButton} style={{ height: `${iconSize}px`, width: `${iconSize}px` }} /> </div> + <div className="timeline-icon" key="timeline_windForward" onClick={this.windForward} title="Speed Up Animation"> <FontAwesomeIcon icon={faForward} style={{ height: `${iconSize}px`, width: `${iconSize}px` }} /> </div> + </div> + <div className="grid-box overview-tool"> + <div className="overview-box"> + <div key="overview-text" className="animation-text">{overviewString}</div> + <TimelineOverview tickSpacing={this._tickSpacing} tickIncrement={this._tickIncrement} time={this._time} parent={this} isAuthoring={BoolCast(this.props.Document.isATOn)} currentBarX={this._currentBarX} totalLength={this._totalLength} visibleLength={this._visibleLength} visibleStart={this._visibleStart} changeCurrentBarX={this.changeCurrentBarX} movePanX={this.movePanX} /> + </div> + <div className="mode-box overview-tool"> + <div key="animation-text" className="animation-text">{modeString}</div> + <div key="round-toggle" ref={this._roundToggleContainerRef} className="round-toggle"> + <div key="round-toggle-slider" ref={this._roundToggleRef} className="round-toggle-slider" onPointerDown={this.toggleChecked}> </div> + </div> + </div> + <div className="time-box overview-tool" style={{ display: this._timelineVisible ? "flex" : "none" }}> + {this.timeIndicator(lengthString, totalTime)} + <div className="resetView-tool" title="Return to Default View" onClick={() => Doc.resetView(this.props.Document)}><FontAwesomeIcon icon="compress-arrows-alt" size="lg" /></div> + <div className="resetView-tool" style={{ display: this._isAuthoring ? "flex" : "none" }} title="Set Default View" onClick={() => Doc.setView(this.props.Document)}><FontAwesomeIcon icon="expand-arrows-alt" size="lg" /></div> + + </div> + </div> + </div> + ); + } + + timeIndicator(lengthString: string, totalTime: number) { + if (this.props.Document.isATOn) { + return ( + <> + <div key="time-text" className="animation-text" style={{ visibility: this.props.Document.isATOn ? "visible" : "hidden", display: this.props.Document.isATOn ? "flex" : "none" }}>{`Total: ${this.toReadTime(totalTime)}`}</div> + </> + ); + } + else { + return ( + <div style={{ flexDirection: "column" }}> + <div className="animation-text" style={{ fontSize: "10px", width: "100%", display: !this.props.Document.isATOn ? "block" : "none" }}>{`Current: ${this.getCurrentTime()}`}</div> + <div className="animation-text" style={{ fontSize: "10px", width: "100%", display: !this.props.Document.isATOn ? "block" : "none" }}>{`Total: ${this.toReadTime(this._time)}`}</div> + </div> + ); + } + } + + /** + * when the user decides to click the toggle button (either user wants to enter editing mode or play mode) + */ + @action + private toggleChecked = (e: React.PointerEvent) => { + e.preventDefault(); + e.stopPropagation(); + this.toggleHandle(); + } + + /** + * turns on the toggle button (the purple slide button that changes from editing mode and play mode + */ + private toggleHandle = () => { + const roundToggle = this._roundToggleRef.current!; + const roundToggleContainer = this._roundToggleContainerRef.current!; + const timelineContainer = this._timelineContainer.current!; + if (BoolCast(this.props.Document.isATOn)) { + //turning on playmode... + roundToggle.style.transform = "translate(0px, 0px)"; + roundToggle.style.animationName = "turnoff"; + roundToggleContainer.style.animationName = "turnoff"; + roundToggleContainer.style.backgroundColor = "white"; + timelineContainer.style.top = `${-this._containerHeight}px`; + this.props.Document.isATOn = false; + this._isAuthoring = false; + this.toPlay(); + } else { + //turning on authoring mode... + roundToggle.style.transform = "translate(20px, 0px)"; + roundToggle.style.animationName = "turnon"; + roundToggleContainer.style.animationName = "turnon"; + roundToggleContainer.style.backgroundColor = "#9acedf"; + timelineContainer.style.top = "0px"; + this.props.Document.isATOn = true; + this._isAuthoring = true; + this.toAuthoring(); + } + } + + + @action.bound + changeLengths() { + if (this._infoContainer.current) { + this._visibleLength = this._infoContainer.current!.getBoundingClientRect().width; //the visible length of the timeline (the length that you current see) + this._visibleStart = this._infoContainer.current!.scrollLeft; //where the div starts + } + } + + // @computed + getCurrentTime = () => { + let current = KeyframeFunc.convertPixelTime(this._currentBarX, "mili", "time", this._tickSpacing, this._tickIncrement); + if (current > this._time) { + current = this._time; + } + return this.toReadTime(current); + } + + @observable private mapOfTracks: (Track | null)[] = []; + + @action + findLongestTime = () => { + let longestTime: number = 0; + this.mapOfTracks.forEach(track => { + if (track) { + const lastTime = track.getLastRegionTime(); + if (this.children.length !== 0) { + if (longestTime <= lastTime) { + longestTime = lastTime; + } + } + } else { + //TODO: remove undefineds and duplicates + } + }); + // console.log(longestTime); + return longestTime; + } + + @action + toAuthoring = () => { + let longestTime = this.findLongestTime(); + if (longestTime === 0) longestTime = 1; + const adjustedTime = Math.ceil(longestTime / 100000) * 100000; + // console.log(adjustedTime); + this._totalLength = KeyframeFunc.convertPixelTime(adjustedTime, "mili", "pixel", this._tickSpacing, this._tickIncrement); + this._time = adjustedTime; + } + + @action + toPlay = () => { + const longestTime = this.findLongestTime(); + this._time = longestTime; + this._totalLength = KeyframeFunc.convertPixelTime(this._time, "mili", "pixel", this._tickSpacing, this._tickIncrement); + } + + /** + * if you have any question here, just shoot me an email or text. + * basically the only thing you need to edit besides render methods in track (individual track lines) and keyframe (green region) + */ + render() { + runInAction(() => { + this._panelWidth = this.props.PanelWidth(); + this.changeLengths(); + // this.toPlay(); + + + // this._time = longestTime; + }); + + const longestTime = this.findLongestTime(); + + // change visible and total width + return ( + <div> + <div style={{ visibility: this._timelineVisible ? "visible" : "hidden" }}> + <div key="timeline_wrapper" style={{ visibility: BoolCast(this.props.Document.isATOn && this._timelineVisible) ? "visible" : "hidden", left: "0px", top: "0px", position: "absolute", width: "100%", transform: "translate(0px, 0px)" }}> + <div key="timeline_container" className="timeline-container" ref={this._timelineContainer} style={{ height: `${this._containerHeight}px`, top: `0px` }}> + <div key="timeline_info" className="info-container" ref={this._infoContainer} onWheel={this.onWheelZoom}> + {this.drawTicks()} + <div key="timeline_scrubber" className="scrubber" style={{ transform: `translate(${this._currentBarX}px)` }}> + <div key="timeline_scrubberhead" className="scrubberhead" onPointerDown={this.onScrubberDown} ></div> + </div> + <div key="timeline_trackbox" className="trackbox" ref={this._trackbox} onPointerDown={this.onPanDown} style={{ width: `${this._totalLength}px` }}> + {DocListCast(this.children).map(doc => { + const track = <Track ref={ref => { this.mapOfTracks.push(ref); }} node={doc} currentBarX={this._currentBarX} changeCurrentBarX={this.changeCurrentBarX} transform={this.props.ScreenToLocalTransform()} time={this._time} tickSpacing={this._tickSpacing} tickIncrement={this._tickIncrement} collection={this.props.Document} timelineVisible={this._timelineVisible} /> + return track; + })} + </div> + </div> + <div className="currentTime">Current: {this.getCurrentTime()}</div> + <div key="timeline_title" className="title-container" ref={this._titleContainer}> + {DocListCast(this.children).map(doc => <div style={{ height: `${(this._titleHeight)}px` }} className="datapane" onPointerOver={() => { Doc.BrushDoc(doc); }} onPointerOut={() => { Doc.UnBrushDoc(doc); }}><p>{doc.title}</p></div>)} + </div> + <div key="timeline_resize" onPointerDown={this.onResizeDown}> + <FontAwesomeIcon className="resize" icon={faGripLines} /> + </div> + </div> + </div> + {this.timelineToolBox(1, longestTime)} + </div> + </div> + ); + } +}
\ No newline at end of file diff --git a/src/client/views/animationtimeline/TimelineMenu.scss b/src/client/views/animationtimeline/TimelineMenu.scss new file mode 100644 index 000000000..7ee0a43d5 --- /dev/null +++ b/src/client/views/animationtimeline/TimelineMenu.scss @@ -0,0 +1,94 @@ +@import "./../globalCssVariables.scss"; + + +.timeline-menu-container{ + position: absolute; + display: flex; + box-shadow: $intermediate-color 0.2vw 0.2vw 0.4vw; + flex-direction: column; + background: whitesmoke; + z-index: 10000; + width: 200px; + padding-bottom: 10px; + border-radius: 15px; + + border: solid #BBBBBBBB 1px; + + + + .timeline-menu-input{ + font: $sans-serif; + font-size: 13px; + width:100%; + text-transform: uppercase; + letter-spacing: 2px; + margin-left: 10px; + background-color: transparent; + border-width: 0px; + transition: border-width 500ms; + } + + .timeline-menu-input:hover{ + border-width: 2px; + } + + + + + .timeline-menu-header{ + border-top-left-radius: 15px; + border-top-right-radius: 15px; + text-transform: uppercase; + background: $dark-color; + letter-spacing: 2px; + + .timeline-menu-header-desc{ + font:$sans-serif; + font-size: 13px; + text-align: center; + color: whitesmoke; + } + } + + + .timeline-menu-item { + // width: 11vw; //10vw + height: 30px; //2vh + background: whitesmoke; + display: flex; //comment out to allow search icon to be inline with search text + justify-content: left; + align-items: center; + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + transition: all .1s; + border-style: none; + // padding: 10px 0px 10px 0px; + white-space: nowrap; + font-size: 13px; + color: grey; + letter-spacing: 2px; + text-transform: uppercase; + padding-right: 20px; + padding-left: 10px; + } + + .timeline-menu-item:hover { + border-width: .11px; + border-style: none; + border-color: $intermediate-color; + border-bottom-style: solid; + border-top-style: solid; + background: $darker-alt-accent; + } + + .timeline-menu-desc { + padding-left: 10px; + font:$sans-serif; + font-size: 13px; + } + +}
\ No newline at end of file diff --git a/src/client/views/animationtimeline/TimelineMenu.tsx b/src/client/views/animationtimeline/TimelineMenu.tsx new file mode 100644 index 000000000..59c25596e --- /dev/null +++ b/src/client/views/animationtimeline/TimelineMenu.tsx @@ -0,0 +1,78 @@ +import * as React from "react"; +import {observable, action, runInAction} from "mobx"; +import {observer} from "mobx-react"; +import "./TimelineMenu.scss"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faChartLine, faRoad, faClipboard, faPen, faTrash, faTable } from "@fortawesome/free-solid-svg-icons"; +import { Utils } from "../../../Utils"; + + +@observer +export class TimelineMenu extends React.Component { + public static Instance:TimelineMenu; + + @observable private _opacity = 0; + @observable private _x = 0; + @observable private _y = 0; + @observable private _currentMenu:JSX.Element[] = []; + + constructor (props:Readonly<{}>){ + super(props); + TimelineMenu.Instance = this; + } + + @action + openMenu = (x?:number, y?:number) => { + this._opacity = 1; + x ? this._x = x : this._x = 0; + y ? this._y = y : this._y = 0; + } + + @action + closeMenu = () => { + this._opacity = 0; + this._currentMenu = []; + this._x = -1000000; + this._y = -1000000; + } + + @action + addItem = (type: "input" | "button", title: string, event: (e:any, ...args:any[]) => void) => { + if (type === "input"){ + let inputRef = React.createRef<HTMLInputElement>(); + let text = ""; + this._currentMenu.push(<div key={Utils.GenerateGuid()} className="timeline-menu-item"><FontAwesomeIcon icon={faClipboard} size="lg"/><input className="timeline-menu-input" ref = {inputRef} placeholder={title} onChange={(e) => { + e.stopPropagation(); + text = e.target.value; + }} onKeyDown={(e) => { + if (e.keyCode === 13) { + event(text); + this.closeMenu(); + e.stopPropagation(); + } + }}/></div>); + } else if (type === "button") { + let buttonRef = React.createRef<HTMLDivElement>(); + this._currentMenu.push( <div key={Utils.GenerateGuid()} className="timeline-menu-item"><FontAwesomeIcon icon={faChartLine}size="lg"/><p className="timeline-menu-desc" onClick={(e) => { + e.preventDefault(); + e.stopPropagation(); + event(e); + this.closeMenu(); + }}>{title}</p></div>); + } + } + + @action + addMenu = (title:string) => { + this._currentMenu.unshift(<div key={Utils.GenerateGuid()} className="timeline-menu-header"><p className="timeline-menu-header-desc">{title}</p></div>); + } + + render() { + return ( + <div key={Utils.GenerateGuid()} className="timeline-menu-container" style={{opacity: this._opacity, left: this._x, top: this._y}} > + {this._currentMenu} + </div> + ); + } + +}
\ No newline at end of file diff --git a/src/client/views/animationtimeline/TimelineOverview.scss b/src/client/views/animationtimeline/TimelineOverview.scss new file mode 100644 index 000000000..283163ea7 --- /dev/null +++ b/src/client/views/animationtimeline/TimelineOverview.scss @@ -0,0 +1,107 @@ +@import "./../globalCssVariables.scss"; + +$timelineColor: #9acedf; +$timelineDark: #77a1aa; + +.timelineOverview-bounding { + width: 100%; + margin-right: 10px; +} + +.timeline-flex { + display: flex; + justify-content: center; + align-items: center; + width: 100%; +} + +.timeline-overview-container { + // padding: 0px; + margin-right: 5px; + // margin: 0px; + margin-left: 5px; + // width: 300px; + width: 100%; + height: 25px; + background: white; + position: relative; + border: 2px solid $timelineDark; + // width: 100%; + + .timeline-overview-visible { + position: absolute; + height: 21px; + background: $timelineColor; + display: inline-block; + margin: 0px; + padding: 0px; + // top: 1px; + } + + .timeline-overview-scrubber-container { + margin: 0px; + padding: 0px; + position: absolute; + height: 100%; + width: 2px; + top: 0px; + left: 0px; + z-index: 1001; + background-color: black; + display: inline-block; + + .timeline-overview-scrubber-head { + padding: 0px; + margin: 0px; + position: absolute; + height: 10px; + width: 10px; + background-color: white; + border-radius: 50%; + border: 2px solid black; + left: -4px; + // top: -30px; + top: -10px; + } + } +} + + + +.timeline-play-bar { + position: relative; + padding: 0px; + margin: 0px; + width: 100%; + height: 4px; + background-color: $timelineColor; + border-radius: 20px; + cursor: pointer; + + .timeline-play-head { + position: absolute; + padding: 0px; + margin: 0px; + width: 20px; + height: 20px; + border-radius: 50%; + background-color: white; + border: 3px solid $timelineColor; + left: 0px; + top: -8px; + cursor: pointer; + } +} + +.timeline-play-tail { + position: absolute; + padding: 0px; + margin: 0px; + height: 4px; + width: 0px; + z-index: 1000; + background-color: $timelineDark; + border-radius: 20px; + margin-top: -4px; + cursor: pointer; +}
\ No newline at end of file diff --git a/src/client/views/animationtimeline/TimelineOverview.tsx b/src/client/views/animationtimeline/TimelineOverview.tsx new file mode 100644 index 000000000..e3a276737 --- /dev/null +++ b/src/client/views/animationtimeline/TimelineOverview.tsx @@ -0,0 +1,181 @@ +import * as React from "react"; +import { observable, action, computed, runInAction, reaction, IReactionDisposer } from "mobx"; +import { observer } from "mobx-react"; +import "./TimelineOverview.scss"; +import * as $ from 'jquery'; +import { Timeline } from "./Timeline"; +import { Keyframe, KeyframeFunc } from "./Keyframe"; + + +interface TimelineOverviewProps { + totalLength: number; + visibleLength: number; + visibleStart: number; + currentBarX: number; + isAuthoring: boolean; + parent: Timeline; + changeCurrentBarX: (pixel: number) => void; + movePanX: (pixel: number) => any; + time: number; + tickSpacing: number; + tickIncrement: number; +} + + +@observer +export class TimelineOverview extends React.Component<TimelineOverviewProps>{ + @observable private _visibleRef = React.createRef<HTMLDivElement>(); + @observable private _scrubberRef = React.createRef<HTMLDivElement>(); + @observable private authoringContainer = React.createRef<HTMLDivElement>(); + @observable private playbackContainer = React.createRef<HTMLDivElement>(); + @observable private overviewBarWidth: number = 0; + @observable private playbarWidth: number = 0; + @observable private activeOverviewWidth: number = 0; + @observable private _authoringReaction?: IReactionDisposer; + @observable private visibleTime: number = 0; + @observable private currentX: number = 0; + @observable private visibleStart: number = 0; + private readonly DEFAULT_HEIGHT = 50; + private readonly DEFAULT_WIDTH = 300; + + componentDidMount = () => { + this.setOverviewWidth(); + + this._authoringReaction = reaction( + () => this.props.parent._isAuthoring, + () => { + if (!this.props.parent._isAuthoring) { + runInAction(() => { + this.setOverviewWidth(); + }); + } + }, + ); + } + + componentWillUnmount = () => { + this._authoringReaction && this._authoringReaction(); + } + + @action + setOverviewWidth() { + const width1 = this.authoringContainer.current?.clientWidth; + const width2 = this.playbackContainer.current?.clientWidth; + if (width1 && width1 !== 0) this.overviewBarWidth = width1; + if (width2 && width2 !== 0) this.playbarWidth = width2; + + if (this.props.isAuthoring) { + this.activeOverviewWidth = this.overviewBarWidth; + } + else { + this.activeOverviewWidth = this.playbarWidth; + } + } + + @action + onPointerDown = (e: React.PointerEvent) => { + e.stopPropagation(); + e.preventDefault(); + document.removeEventListener("pointermove", this.onPanX); + document.removeEventListener("pointerup", this.onPointerUp); + document.addEventListener("pointermove", this.onPanX); + document.addEventListener("pointerup", this.onPointerUp); + } + + @action + onPanX = (e: PointerEvent) => { + e.stopPropagation(); + e.preventDefault(); + const movX = (this.props.visibleStart / this.props.totalLength) * (this.DEFAULT_WIDTH) + e.movementX; + this.props.movePanX((movX / (this.DEFAULT_WIDTH)) * this.props.totalLength); + } + + @action + onPointerUp = (e: PointerEvent) => { + e.stopPropagation(); + e.preventDefault(); + document.removeEventListener("pointermove", this.onPanX); + document.removeEventListener("pointerup", this.onPointerUp); + } + + @action + onScrubberDown = (e: React.PointerEvent) => { + e.preventDefault(); + e.stopPropagation(); + document.removeEventListener("pointermove", this.onScrubberMove); + document.removeEventListener("pointerup", this.onScrubberUp); + document.addEventListener("pointermove", this.onScrubberMove); + document.addEventListener("pointerup", this.onScrubberUp); + } + + @action + onScrubberMove = (e: PointerEvent) => { + e.preventDefault(); + e.stopPropagation(); + const scrubberRef = this._scrubberRef.current!; + const left = scrubberRef.getBoundingClientRect().left; + const offsetX = Math.round(e.clientX - left); + this.props.changeCurrentBarX((((offsetX) / this.activeOverviewWidth) * this.props.totalLength) + this.props.currentBarX); + } + + @action + onScrubberUp = (e: PointerEvent) => { + e.preventDefault(); + e.stopPropagation(); + document.removeEventListener("pointermove", this.onScrubberMove); + document.removeEventListener("pointerup", this.onScrubberUp); + } + + @action + getTimes() { + const vis = KeyframeFunc.convertPixelTime(this.props.visibleLength, "mili", "time", this.props.tickSpacing, this.props.tickIncrement); + const x = KeyframeFunc.convertPixelTime(this.props.currentBarX, "mili", "time", this.props.tickSpacing, this.props.tickIncrement); + const start = KeyframeFunc.convertPixelTime(this.props.visibleStart, "mili", "time", this.props.tickSpacing, this.props.tickIncrement); + this.visibleTime = vis; + this.currentX = x; + this.visibleStart = start; + } + + render() { + this.setOverviewWidth(); + this.getTimes(); + + const percentVisible = this.visibleTime / this.props.time; + const visibleBarWidth = percentVisible * this.activeOverviewWidth; + + const percentScrubberStart = this.currentX / this.props.time; + let scrubberStart = this.props.currentBarX / this.props.totalLength * this.activeOverviewWidth; + if (scrubberStart > this.activeOverviewWidth) scrubberStart = this.activeOverviewWidth; + + const percentBarStart = this.visibleStart / this.props.time; + const barStart = percentBarStart * this.activeOverviewWidth; + + let playWidth = (this.props.currentBarX / this.props.totalLength) * this.activeOverviewWidth; + if (playWidth > this.activeOverviewWidth) playWidth = this.activeOverviewWidth; + + const timeline = this.props.isAuthoring ? [ + + <div key="timeline-overview-container" className="timeline-overview-container overviewBar" id="timelineOverview" ref={this.authoringContainer}> + <div ref={this._visibleRef} key="timeline-overview-visible" className="timeline-overview-visible" style={{ left: `${barStart}px`, width: `${visibleBarWidth}px` }} onPointerDown={this.onPointerDown}></div>, + <div ref={this._scrubberRef} key="timeline-overview-scrubber-container" className="timeline-overview-scrubber-container" style={{ left: `${scrubberStart}px` }} onPointerDown={this.onScrubberDown}> + <div key="timeline-overview-scrubber-head" className="timeline-overview-scrubber-head"></div> + </div> + </div> + ] : [ + <div key="timeline-play-container" className="timeline-play-bar overviewBar" id="timelinePlay" ref={this.playbackContainer}> + <div ref={this._scrubberRef} className="timeline-play-head" style={{ left: `${scrubberStart}px` }} onPointerDown={this.onScrubberDown}></div> + </div>, + <div className="timeline-play-tail" style={{ width: `${playWidth}px` }}></div> + ]; + return ( + <div className="timeline-flex"> + <div className="timelineOverview-bounding"> + {timeline} + </div> + </div> + ); + } + +} + + diff --git a/src/client/views/animationtimeline/Track.scss b/src/client/views/animationtimeline/Track.scss new file mode 100644 index 000000000..aec587a79 --- /dev/null +++ b/src/client/views/animationtimeline/Track.scss @@ -0,0 +1,15 @@ +@import "./../globalCssVariables.scss"; + +.track-container { + + .track { + .inner { + top: 0px; + width: calc(100%); + background-color: $light-color; + border: 1px solid $dark-color; + position: relative; + z-index: 100; + } + } +}
\ No newline at end of file diff --git a/src/client/views/animationtimeline/Track.tsx b/src/client/views/animationtimeline/Track.tsx new file mode 100644 index 000000000..705cc33a2 --- /dev/null +++ b/src/client/views/animationtimeline/Track.tsx @@ -0,0 +1,422 @@ +import * as React from "react"; +import { observer } from "mobx-react"; +import { observable, reaction, action, IReactionDisposer, computed, runInAction, autorun, toJS, isObservableArray, IObservableArray, trace, observe, intercept } from "mobx"; +import "./Track.scss"; +import { Doc, DocListCastAsync, DocListCast, Field } from "../../../new_fields/Doc"; +import { listSpec } from "../../../new_fields/Schema"; +import { FieldValue, Cast, NumCast, BoolCast, StrCast } from "../../../new_fields/Types"; +import { List } from "../../../new_fields/List"; +import { Keyframe, KeyframeFunc, RegionData } from "./Keyframe"; +import { Transform } from "../../util/Transform"; +import { Copy } from "../../../new_fields/FieldSymbols"; +import { ObjectField } from "../../../new_fields/ObjectField"; + +interface IProps { + node: Doc; + currentBarX: number; + transform: Transform; + collection: Doc; + time: number; + tickIncrement: number; + tickSpacing: number; + timelineVisible: boolean; + changeCurrentBarX: (x: number) => void; +} + +@observer +export class Track extends React.Component<IProps> { + @observable private _inner = React.createRef<HTMLDivElement>(); + @observable private _currentBarXReaction: any; + @observable private _timelineVisibleReaction: any; + @observable private _autoKfReaction: any; + @observable private _newKeyframe: boolean = false; + private readonly MAX_TITLE_HEIGHT = 75; + private _trackHeight = 0; + private primitiveWhitelist = [ + "x", + "y", + "width", + "height", + "opacity", + ]; + private objectWhitelist = [ + "data" + ]; + + @computed private get regions() { return Cast(this.props.node.regions, listSpec(Doc)) as List<Doc>; } + @computed private get time() { return NumCast(KeyframeFunc.convertPixelTime(this.props.currentBarX, "mili", "time", this.props.tickSpacing, this.props.tickIncrement)); } + + ////////// life cycle functions/////////////// + componentWillMount() { + runInAction(() => { + if (!this.props.node.regions) this.props.node.regions = new List<Doc>(); //if there is no region, then create new doc to store stuff + //these two lines are exactly same from timeline.tsx + let relativeHeight = window.innerHeight / 20; + this._trackHeight = relativeHeight < this.MAX_TITLE_HEIGHT ? relativeHeight : this.MAX_TITLE_HEIGHT; //for responsiveness + }); + } + + componentDidMount() { + runInAction(async () => { + this._timelineVisibleReaction = this.timelineVisibleReaction(); + this._currentBarXReaction = this.currentBarXReaction(); + if (this.regions.length === 0) this.createRegion(this.time); + this.props.node.hidden = false; + this.props.node.opacity = 1; + // this.autoCreateKeyframe(); + }); + } + + /** + * mainly for disposing reactions + */ + componentWillUnmount() { + runInAction(() => { + //disposing reactions + if (this._currentBarXReaction) this._currentBarXReaction(); + if (this._timelineVisibleReaction) this._timelineVisibleReaction(); + if (this._autoKfReaction) this._autoKfReaction(); + }); + } + //////////////////////////////// + + + getLastRegionTime = () => { + let lastTime:number = 0; + let lastRegion:(Doc | undefined); + DocListCast(this.regions).forEach(region => { + const time = NumCast(region.position); + if (lastTime <= time) { + lastTime = time; + lastRegion = region; + } + }); + return lastRegion ? lastTime + NumCast(lastRegion.duration) : 0; + } + + /** + * keyframe save logic. Needs to be changed so it's more efficient + * + */ + @action + saveKeyframe = async () => { + console.log("saving keyframe"); + let keyframes: List<Doc> = (Cast(this.saveStateRegion!.keyframes, listSpec(Doc)) as List<Doc>); + let kfIndex: number = keyframes.indexOf(this.saveStateKf!); + let kf = keyframes[kfIndex] as Doc; //index in the keyframe + if (this._newKeyframe) { + console.log("new keyframe registering"); + let kfList = DocListCast(this.saveStateRegion!.keyframes); + kfList.forEach(kf => { + kf.key = this.makeCopy(); + if (kfList.indexOf(kf) === 0 || kfList.indexOf(kf) === 3){ + (kf.key as Doc).opacity = 0.1; + } else{ + (kf.key as Doc).opacity = 1; + } + }); + this._newKeyframe = false; + } + if (!kf) return; + if (kf.type === KeyframeFunc.KeyframeType.default) { // only save for non-fades + kf.key = this.makeCopy(); + let leftkf: (Doc | undefined) = await KeyframeFunc.calcMinLeft(this.saveStateRegion!, this.time, kf); // lef keyframe, if it exists + let rightkf: (Doc | undefined) = await KeyframeFunc.calcMinRight(this.saveStateRegion!, this.time, kf); //right keyframe, if it exists + if (leftkf!.type === KeyframeFunc.KeyframeType.fade) { //replicating this keyframe to fades + let edge: (Doc | undefined) = await KeyframeFunc.calcMinLeft(this.saveStateRegion!, this.time, leftkf!); + edge!.key = this.makeCopy(); + leftkf!.key = this.makeCopy(); + (Cast(edge!.key, Doc)! as Doc).opacity = 0.1; + (Cast(leftkf!.key, Doc)! as Doc).opacity = 1; + } + if (rightkf!.type === KeyframeFunc.KeyframeType.fade) { + let edge: (Doc | undefined) = await KeyframeFunc.calcMinRight(this.saveStateRegion!, this.time, rightkf!); + edge!.key = this.makeCopy(); + rightkf!.key = this.makeCopy(); + (Cast(edge!.key, Doc)! as Doc).opacity = 0.1; + (Cast(rightkf!.key, Doc)! as Doc).opacity = 1; + } + } + keyframes[kfIndex] = kf; + this.saveStateKf = undefined; + this.saveStateRegion = undefined; + } + + + /** + * autocreates keyframe + */ + @action + autoCreateKeyframe = () => { + const { node } = this.props; + const objects = this.objectWhitelist.map(key => node[key]); + intercept(this.props.node, change => { + console.log(change); + return change; + }); + return reaction(() => { + return [...this.primitiveWhitelist.map(key => node[key]), ...objects]; + }, (changed, reaction) => { + //check for region + this.findRegion(this.time).then((region) => { + if (region !== undefined) { //if region at scrub time exist + let r = region as any as RegionData; //for some region is returning undefined... which is not the case + if (DocListCast(r.keyframes).find(kf => kf.time === this.time) === undefined) { //basically when there is no additional keyframe at that timespot + this.makeKeyData(r, this.time, KeyframeFunc.KeyframeType.default); + } + } + }); + }, { fireImmediately: false }); + } + + + + // @observable private _storedState:(Doc | undefined) = undefined; + // /** + // * reverting back to previous state before editing on AT + // */ + // @action + // revertState = () => { + // if (this._storedState) this.applyKeys(this._storedState); + // } + + + /** + * Reaction when scrubber bar changes + * made into function so it's easier to dispose later + */ + @action + currentBarXReaction = () => { + return reaction(() => this.props.currentBarX, () => { + this.findRegion(this.time).then((regiondata: (Doc | undefined)) => { + if (regiondata) { + this.props.node.hidden = false; + // if (!this._autoKfReaction) { + // // console.log("creating another reaction"); + // // this._autoKfReaction = this.autoCreateKeyframe(); + // } + this.timeChange(); + } else { + this.props.node.hidden = true; + this.props.node.opacity = 0; + //if (this._autoKfReaction) this._autoKfReaction(); + } + }); + }); + } + + /** + * when timeline is visible, reaction is ran so states are reverted + */ + @action + timelineVisibleReaction = () => { + return reaction(() => { + return this.props.timelineVisible; + }, isVisible => { + if (isVisible) { + DocListCast(this.regions).forEach(region => { + if (!BoolCast((Cast(region, Doc) as Doc).hasData)) { + for (let i = 0; i < 4; i++) { + DocListCast(((Cast(region, Doc) as Doc).keyframes as List<Doc>))[i].key = this.makeCopy(); + if (i === 0 || i === 3) { //manually inputing fades + (DocListCast(((Cast(region, Doc) as Doc).keyframes as List<Doc>))[i].key! as Doc).opacity = 0.1; + } + } + } + }); + } else { + console.log("reverting state"); + //this.revertState(); + } + }); + } + + @observable private saveStateKf: (Doc | undefined) = undefined; + @observable private saveStateRegion: (Doc | undefined) = undefined; + + /**w + * when scrubber position changes. Need to edit the logic + */ + @action + timeChange = async () => { + if (this.saveStateKf !== undefined) { + await this.saveKeyframe(); + } else if (this._newKeyframe){ + await this.saveKeyframe(); + } + let regiondata = await this.findRegion(Math.round(this.time)); //finds a region that the scrubber is on + if (regiondata) { + let leftkf: (Doc | undefined) = await KeyframeFunc.calcMinLeft(regiondata, this.time); // lef keyframe, if it exists + let rightkf: (Doc | undefined) = await KeyframeFunc.calcMinRight(regiondata, this.time); //right keyframe, if it exists + let currentkf: (Doc | undefined) = await this.calcCurrent(regiondata); //if the scrubber is on top of the keyframe + if (currentkf) { + console.log("is current"); + await this.applyKeys(currentkf); + this.saveStateKf = currentkf; + this.saveStateRegion = regiondata; + } else if (leftkf && rightkf) { + await this.interpolate(leftkf, rightkf); + } + } + } + + /** + * applying changes (when saving the keyframe) + * need to change the logic here + */ + @action + private applyKeys = async (kf: Doc) => { + let kfNode = await Cast(kf.key, Doc) as Doc; + this.primitiveWhitelist.forEach(key => { + if (!kfNode[key]) { + this.props.node[key] = undefined; + } else { + let stored = kfNode[key]; + this.props.node[key] = stored instanceof ObjectField ? stored[Copy]() : stored; + } + }); + } + + + /** + * calculating current keyframe, if the scrubber is right on the keyframe + */ + @action + calcCurrent = (region: Doc) => { + let currentkf: (Doc | undefined) = undefined; + let keyframes = DocListCast(region.keyframes!); + keyframes.forEach((kf) => { + if (NumCast(kf.time) === Math.round(this.time)) currentkf = kf; + }); + return currentkf; + } + + + /** + * basic linear interpolation function + */ + @action + interpolate = async (left: Doc, right: Doc) => { + let leftNode = await (left.key) as Doc; + let rightNode = await (right.key) as Doc; + this.primitiveWhitelist.forEach(key => { + if (leftNode[key] && rightNode[key] && typeof (leftNode[key]) === "number" && typeof (rightNode[key]) === "number") { //if it is number, interpolate + let dif = NumCast(rightNode[key]) - NumCast(leftNode[key]); + let deltaLeft = this.time - NumCast(left.time); + let ratio = deltaLeft / (NumCast(right.time) - NumCast(left.time)); + this.props.node[key] = NumCast(leftNode[key]) + (dif * ratio); + } else { // case data + let stored = leftNode[key]; + this.props.node[key] = stored instanceof ObjectField ? stored[Copy]() : stored; + } + }); + } + + /** + * finds region that corresponds to specific time (is there a region at this time?) + * linear O(n) (maybe possible to optimize this with other Data structures?) + */ + @action + findRegion = async (time: number) => { + let foundRegion: (Doc | undefined) = undefined; + let regions = await DocListCastAsync(this.regions); + regions!.forEach(region => { + region = region as RegionData; + if (time >= NumCast(region.position) && time <= (NumCast(region.position) + NumCast(region.duration))) { + foundRegion = region; + } + }); + return foundRegion; + } + + + /** + * double click on track. Signalling keyframe creation. + */ + @action + onInnerDoubleClick = (e: React.MouseEvent) => { + let inner = this._inner.current!; + let offsetX = Math.round((e.clientX - inner.getBoundingClientRect().left) * this.props.transform.Scale); + this.createRegion(KeyframeFunc.convertPixelTime(offsetX, "mili", "time", this.props.tickSpacing, this.props.tickIncrement)); + } + + + /** + * creates a region (KEYFRAME.TSX stuff). + */ + @action + createRegion = async (time: number) => { + if (await this.findRegion(time) === undefined) { //check if there is a region where double clicking (prevents phantom regions) + let regiondata = KeyframeFunc.defaultKeyframe(); //create keyframe data + + regiondata.position = time; //set position + let rightRegion = KeyframeFunc.findAdjacentRegion(KeyframeFunc.Direction.right, regiondata, this.regions); + + if (rightRegion && rightRegion.position - regiondata.position <= 4000) { //edge case when there is less than default 4000 duration space between this and right region + regiondata.duration = rightRegion.position - regiondata.position; + } + if (this.regions.length === 0 || !rightRegion || (rightRegion && rightRegion.position - regiondata.position >= NumCast(regiondata.fadeIn) + NumCast(regiondata.fadeOut))) { + this.regions.push(regiondata); + this._newKeyframe = true; + this.saveStateRegion = regiondata; + return regiondata; + } + } + } + + @action + makeKeyData = (regiondata: RegionData, time: number, type: KeyframeFunc.KeyframeType = KeyframeFunc.KeyframeType.default) => { //Kfpos is mouse offsetX, representing time + let doclist = DocListCast(regiondata.keyframes)!; + let existingkf: (Doc | undefined) = undefined; + doclist.forEach(TK => { + if (TK.time === time) existingkf = TK; + }); + if (existingkf) return existingkf; + //else creates a new doc. + let TK: Doc = new Doc(); + TK.time = time; + TK.key = this.makeCopy(); + TK.type = type; + //assuming there are already keyframes (for keeping keyframes in order, sorted by time) + if (doclist.length === 0) regiondata.keyframes!.push(TK); + doclist.forEach(kf => { + let index = doclist.indexOf(kf); + let kfTime = NumCast(kf.time); + if ((kfTime < time && index === doclist.length - 1) || (kfTime < time && time < NumCast(doclist[index + 1].time))) { + regiondata.keyframes!.splice(index + 1, 0, TK); + return; + } + }); + return TK; + } + + @action + makeCopy = () => { + let doc = new Doc(); + this.primitiveWhitelist.forEach(key => { + let originalVal = this.props.node[key]; + if (key === "data"){ + console.log(originalVal); + } + doc[key] = originalVal instanceof ObjectField ? originalVal[Copy]() : this.props.node[key]; + }); + return doc; + } + + /** + * UI sstuff here. Not really much to change + */ + render() { + return ( + <div className="track-container"> + <div className="track"> + <div className="inner" ref={this._inner} onDoubleClick={this.onInnerDoubleClick} onPointerOver={() => { Doc.BrushDoc(this.props.node); }} onPointerOut={() => { Doc.UnBrushDoc(this.props.node); }} style={{ height: `${this._trackHeight}px` }}> + {DocListCast(this.regions).map((region) => { + return <Keyframe {...this.props} RegionData={region} makeKeyData={this.makeKeyData} />; + })} + </div> + </div> + </div> + ); + } +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionMapView.tsx b/src/client/views/collections/CollectionMapView.tsx index 7b7828d7d..971224482 100644 --- a/src/client/views/collections/CollectionMapView.tsx +++ b/src/client/views/collections/CollectionMapView.tsx @@ -1,4 +1,4 @@ -import { GoogleApiWrapper, Map as GeoMap, MapProps, Marker } from "google-maps-react"; +import { GoogleApiWrapper, Map as GeoMap, IMapProps, Marker } from "google-maps-react"; import { observer } from "mobx-react"; import { Doc, Opt, DocListCast, FieldResult, Field } from "../../../new_fields/Doc"; import { documentSchema } from "../../../new_fields/documentSchemas"; @@ -42,7 +42,7 @@ const query = async (data: string | google.maps.LatLngLiteral) => { }; @observer -class CollectionMapView extends CollectionSubView<MapSchema, Partial<MapProps> & { google: any }>(MapSchema) { +class CollectionMapView extends CollectionSubView<MapSchema, Partial<IMapProps> & { google: any }>(MapSchema) { private _cancelAddrReq = new Map<string, boolean>(); private _cancelLocReq = new Map<string, boolean>(); @@ -221,7 +221,7 @@ class CollectionMapView extends CollectionSubView<MapSchema, Partial<MapProps> & zoom={center.zoom || 10} initialCenter={center} center={center} - onIdle={(_props?: MapProps, map?: google.maps.Map) => { + onIdle={(_props?: IMapProps, map?: google.maps.Map) => { if (this.layoutDoc.lockedTransform) { // reset zoom (ideally, we could probably can tell the map to disallow zooming somehow instead) map?.setZoom(center?.zoom || 10); @@ -233,7 +233,7 @@ class CollectionMapView extends CollectionSubView<MapSchema, Partial<MapProps> & }))(); } }} - onDragend={(_props?: MapProps, map?: google.maps.Map) => { + onDragend={(_props?: IMapProps, map?: google.maps.Map) => { if (this.layoutDoc.lockedTransform) { // reset the drag (ideally, we could probably can tell the map to disallow dragging somehow instead) map?.setCenter({ lat: center?.lat!, lng: center?.lng! }); diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index fb7535d9f..9ddc1296e 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -13,11 +13,12 @@ import { DocumentType } from "../../documents/DocumentTypes"; import { Docs, DocumentOptions } from "../../documents/Documents"; import { DragManager, dropActionType } from "../../util/DragManager"; import { undoBatch, UndoManager } from "../../util/UndoManager"; -import { DocComponent } from "../DocComponent"; import { FieldViewProps } from "../nodes/FieldView"; import { FormattedTextBox, GoogleRef } from "../nodes/formattedText/FormattedTextBox"; import { CollectionView } from "./CollectionView"; import React = require("react"); +import { DocComponent } from "../DocComponent"; +var path = require('path'); import { basename } from 'path'; import { GooglePhotos } from "../../apis/google_docs/GooglePhotosClientUtils"; import { ImageUtils } from "../../util/Import & Export/ImageUtils"; diff --git a/src/client/views/collections/CollectionViewChromes.scss b/src/client/views/collections/CollectionViewChromes.scss index 5203eb55f..e4581eb46 100644 --- a/src/client/views/collections/CollectionViewChromes.scss +++ b/src/client/views/collections/CollectionViewChromes.scss @@ -33,6 +33,17 @@ outline-color: black; } + .collectionViewBaseChrome-button{ + font-size: 75%; + text-transform: uppercase; + letter-spacing: 2px; + background: rgb(238, 238, 238); + color: purple; + outline-color: black; + border: none; + padding: 12px 10px 11px 10px; + margin-left: 10px; + } .collectionViewBaseChrome-cmdPicker { margin-left: 3px; margin-right: 0px; diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 92b27a0c6..b9e80bb43 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -44,6 +44,7 @@ import MarqueeOptionsMenu from "./MarqueeOptionsMenu"; import { MarqueeView } from "./MarqueeView"; import React = require("react"); import { CollectionViewType } from "../CollectionView"; +import { Timeline } from "../../animationtimeline/Timeline"; library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard, faFileUpload); @@ -94,6 +95,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P public get displayName() { return "CollectionFreeFormView(" + this.props.Document.title?.toString() + ")"; } // this makes mobx trace() statements more descriptive @observable.shallow _layoutElements: ViewDefResult[] = []; // shallow because some layout items (eg pivot labels) are just generated 'divs' and can't be frozen as observables @observable _clusterSets: (Doc[])[] = []; + @observable _timelineRef = React.createRef<Timeline>(); @computed get fitToContentScaling() { return this.fitToContent ? NumCast(this.layoutDoc.fitToContentScaling, 1) : 1; } @computed get fitToContent() { return (this.props.fitToBox || this.Document._fitToBox) && !this.isAnnotationOverlay; } @@ -1126,8 +1128,8 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P input.click(); } }); - - ContextMenu.Instance.addItem({ description: "Options...", subitems: optionItems, icon: "eye" }); + ContextMenu.Instance.addItem({ description: "Options ...", subitems: optionItems, icon: "eye" }); + this._timelineRef.current!.timelineContextMenu(e); } private childViews = () => { diff --git a/src/client/views/graph/Graph.tsx b/src/client/views/graph/Graph.tsx new file mode 100644 index 000000000..d925cc32c --- /dev/null +++ b/src/client/views/graph/Graph.tsx @@ -0,0 +1,32 @@ +import * as React from "react"; +import {observable} from "mobx"; +import { observer } from "mobx-react"; +import { Document, listSpec } from "../../../new_fields/Schema"; +import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView"; +import { CollectionSubView, CollectionViewProps, SubCollectionViewProps } from "../collections/CollectionSubView"; + + + + +export class Graph extends CollectionSubView(Document) { + static Instance:Graph; + + private constructor(props:SubCollectionViewProps) { + super(props); + Graph.Instance = this; + } + + + + + render() { + let collection = <CollectionFreeFormView {...this.props}/>; + + return ( + <div> + </div> + + ); + } + +}
\ No newline at end of file diff --git a/src/client/views/graph/GraphManager.ts b/src/client/views/graph/GraphManager.ts new file mode 100644 index 000000000..b62f2337b --- /dev/null +++ b/src/client/views/graph/GraphManager.ts @@ -0,0 +1,45 @@ + + +import {Graph} from "./Graph"; +import {observable, computed} from 'mobx'; +import { Dictionary } from "typescript-collections"; +import { string } from "prop-types"; +import { Doc } from "../../../new_fields/Doc"; + + +export class GraphManager { + @observable public Graphs: Graph[] = []; + + @observable public GraphData: Doc = new Doc(); + + private static _instance: GraphManager; + + @computed + public static get Instance():GraphManager { + return this._instance || (this._instance = new this()); + } + + private constructor(){ + + } + + + + + public set addGraph(graph:Graph){ + this.Graphs.push(graph); + } + + + defaultGraphs = () => { + + } + + + + + + + + +}
\ No newline at end of file diff --git a/src/client/views/graph/GraphMenu.tsx b/src/client/views/graph/GraphMenu.tsx new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/src/client/views/graph/GraphMenu.tsx diff --git a/src/client/views/linking/LinkFollowBox.tsx b/src/client/views/linking/LinkFollowBox.tsx new file mode 100644 index 000000000..705469fd2 --- /dev/null +++ b/src/client/views/linking/LinkFollowBox.tsx @@ -0,0 +1,571 @@ +import { observable, computed, action, runInAction, reaction, IReactionDisposer } from "mobx"; +import React = require("react"); +import { observer } from "mobx-react"; +import { FieldViewProps, FieldView } from "../nodes/FieldView"; +import { Doc, DocListCastAsync, Opt } from "../../../new_fields/Doc"; +import { undoBatch } from "../../util/UndoManager"; +import { NumCast, FieldValue, Cast, StrCast } from "../../../new_fields/Types"; +import { CollectionViewType } from "../collections/CollectionView"; +import { CollectionDockingView } from "../collections/CollectionDockingView"; +import { SelectionManager } from "../../util/SelectionManager"; +import { DocumentManager } from "../../util/DocumentManager"; +import { DocumentView } from "../nodes/DocumentView"; +import "./LinkFollowBox.scss"; +import { SearchUtil } from "../../util/SearchUtil"; +import { Id } from "../../../new_fields/FieldSymbols"; +import { listSpec } from "../../../new_fields/Schema"; +import { DocServer } from "../../DocServer"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faTimes } from '@fortawesome/free-solid-svg-icons'; +import { docs_v1 } from "googleapis"; +import { Utils } from "../../../Utils"; +import { Link } from "@react-pdf/renderer"; + +export enum FollowModes { + OPENTAB = "Open in Tab", + OPENRIGHT = "Open in Right Split", + OPENFULL = "Open Full Screen", + PAN = "Pan to Document", + INPLACE = "Open In Place" +} + +enum FollowOptions { + ZOOM = "Zoom", + NOZOOM = "No Zoom", +} + +@observer +export class LinkFollowBox extends React.Component<FieldViewProps> { + + public static LayoutString(fieldKey: string) { return FieldView.LayoutString(LinkFollowBox, fieldKey); } + public static Instance: LinkFollowBox | undefined; + @observable static linkDoc: Doc | undefined = undefined; + @observable static destinationDoc: Doc | undefined = undefined; + @observable static sourceDoc: Doc | undefined = undefined; + @observable selectedMode: string = ""; + @observable selectedContext: Doc | undefined = undefined; + @observable selectedContextAliases: Doc[] | undefined = undefined; + @observable selectedOption: string = ""; + @observable selectedContextString: string = ""; + @observable sourceView: DocumentView | undefined = undefined; + @observable canPan: boolean = false; + @observable shouldUseOnlyParentContext = false; + _contextDisposer?: IReactionDisposer; + + @observable private _docs: { col: Doc, target: Doc }[] = []; + @observable private _otherDocs: { col: Doc, target: Doc }[] = []; + + constructor(props: FieldViewProps) { + super(props); + LinkFollowBox.Instance = this; + this.resetVars(); + this.props.Document.isBackground = true; + } + + componentDidMount = () => { + this.resetVars(); + + this._contextDisposer = reaction( + () => this.selectedContextString, + async () => { + const ref = await DocServer.GetRefField(this.selectedContextString); + runInAction(() => { + if (ref instanceof Doc) { + this.selectedContext = ref; + } + }); + if (this.selectedContext instanceof Doc) { + const aliases = await SearchUtil.GetViewsOfDocument(this.selectedContext); + runInAction(() => { this.selectedContextAliases = aliases; }); + } + } + ); + } + + componentWillUnmount = () => { + this._contextDisposer && this._contextDisposer(); + } + + async resetPan() { + if (LinkFollowBox.destinationDoc && this.sourceView && this.sourceView.props.ContainingCollectionDoc) { + runInAction(() => this.canPan = false); + if (this.sourceView.props.ContainingCollectionDoc._viewType === CollectionViewType.Freeform) { + const docs = Cast(this.sourceView.props.ContainingCollectionDoc.data, listSpec(Doc), []); + const aliases = await SearchUtil.GetViewsOfDocument(Doc.GetProto(LinkFollowBox.destinationDoc)); + + aliases.forEach(alias => { + if (docs.filter(doc => doc === alias).length > 0) { + runInAction(() => { this.canPan = true; }); + } + }); + } + } + } + + @action + resetVars = () => { + this.selectedContext = undefined; + this.selectedContextString = ""; + this.selectedMode = ""; + this.selectedOption = ""; + LinkFollowBox.linkDoc = undefined; + LinkFollowBox.sourceDoc = undefined; + LinkFollowBox.destinationDoc = undefined; + this.sourceView = undefined; + this.canPan = false; + this.shouldUseOnlyParentContext = false; + } + + async fetchDocuments() { + if (LinkFollowBox.destinationDoc) { + const dest: Doc = LinkFollowBox.destinationDoc; + const aliases = await SearchUtil.GetViewsOfDocument(Doc.GetProto(dest)); + const { docs } = await SearchUtil.Search("", true, { fq: `data_l:"${dest[Id]}"` }); + const map: Map<Doc, Doc> = new Map; + const allDocs = await Promise.all(aliases.map(doc => SearchUtil.Search("", true, { fq: `data_l:"${doc[Id]}"` }).then(result => result.docs))); + allDocs.forEach((docs, index) => docs.forEach(doc => map.set(doc, aliases[index]))); + docs.forEach(doc => map.delete(doc)); + runInAction(async () => { + this._docs = docs.filter(doc => !Doc.AreProtosEqual(doc, CollectionDockingView.Instance.props.Document)).map(doc => ({ col: doc, target: dest })); + this._otherDocs = Array.from(map.entries()).filter(entry => !Doc.AreProtosEqual(entry[0], CollectionDockingView.Instance.props.Document)).map(([col, target]) => ({ col, target })); + const tcontext = LinkFollowBox.linkDoc && (await Cast(LinkFollowBox.linkDoc.anchor2Context, Doc)) as Doc; + runInAction(() => tcontext && this._docs.splice(0, 0, { col: tcontext, target: dest })); + }); + } + } + + @action + setLinkDocs = (linkDoc: Doc, source: Doc, dest: Doc) => { + this.resetVars(); + + LinkFollowBox.linkDoc = linkDoc; + LinkFollowBox.sourceDoc = source; + LinkFollowBox.destinationDoc = dest; + this.fetchDocuments(); + + SelectionManager.SelectedDocuments().forEach(dv => { + if (dv.props.Document === LinkFollowBox.sourceDoc) { + this.sourceView = dv; + } + }); + + this.resetPan(); + } + + highlightDoc = () => LinkFollowBox.destinationDoc && Doc.linkFollowHighlight(LinkFollowBox.destinationDoc); + + @undoBatch + openFullScreen = () => { + if (LinkFollowBox.destinationDoc) { + const view = DocumentManager.Instance.getDocumentView(LinkFollowBox.destinationDoc); + view && CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(view); + } + } + + @undoBatch + openColFullScreen = (options: { context: Doc }) => { + if (LinkFollowBox.destinationDoc) { + if (NumCast(options.context._viewType, CollectionViewType.Invalid) === CollectionViewType.Freeform) { + const newPanX = NumCast(LinkFollowBox.destinationDoc.x) + NumCast(LinkFollowBox.destinationDoc._width) / 2; + const newPanY = NumCast(LinkFollowBox.destinationDoc.y) + NumCast(LinkFollowBox.destinationDoc._height) / 2; + options.context._panX = newPanX; + options.context._panY = newPanY; + } + const view = DocumentManager.Instance.getDocumentView(options.context); + view && CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(view); + this.highlightDoc(); + } + } + + // should container be a doc or documentview or what? This one needs work and is more long term + @undoBatch + openInContainer = (options: { container: Doc }) => { + + } + + static _addDocTab: (undefined | ((doc: Doc, dataDoc: Opt<Doc>, where: string) => boolean)); + + static setAddDocTab = (addFunc: (doc: Doc, dataDoc: Opt<Doc>, where: string) => boolean) => { + LinkFollowBox._addDocTab = addFunc; + } + + @undoBatch + openLinkColRight = (options: { context: Doc, shouldZoom: boolean }) => { + if (LinkFollowBox.destinationDoc) { + options.context = Doc.IsPrototype(options.context) ? Doc.MakeDelegate(options.context) : options.context; + if (NumCast(options.context._viewType, CollectionViewType.Invalid) === CollectionViewType.Freeform) { + const newPanX = NumCast(LinkFollowBox.destinationDoc.x) + NumCast(LinkFollowBox.destinationDoc._width) / 2; + const newPanY = NumCast(LinkFollowBox.destinationDoc.y) + NumCast(LinkFollowBox.destinationDoc._height) / 2; + options.context._panX = newPanX; + options.context._panY = newPanY; + } + (LinkFollowBox._addDocTab || this.props.addDocTab)(options.context, undefined, "onRight"); + + if (options.shouldZoom) this.jumpToLink({ shouldZoom: options.shouldZoom }); + + this.highlightDoc(); + SelectionManager.DeselectAll(); + } + } + + @undoBatch + openLinkRight = () => { + if (LinkFollowBox.destinationDoc) { + const alias = Doc.MakeAlias(LinkFollowBox.destinationDoc); + (LinkFollowBox._addDocTab || this.props.addDocTab)(alias, undefined, "onRight"); + this.highlightDoc(); + SelectionManager.DeselectAll(); + } + + } + + @undoBatch + jumpToLink = async (options: { shouldZoom: boolean }) => { + if (LinkFollowBox.sourceDoc && LinkFollowBox.linkDoc) { + const focus = (document: Doc) => { (LinkFollowBox._addDocTab || this.props.addDocTab)(document, undefined, "inTab"); SelectionManager.DeselectAll(); }; + //let focus = (doc: Doc, maxLocation: string) => this.props.focus(docthis.props.focus(LinkFollowBox.destinationDoc, true, 1, () => this.props.addDocTab(doc, undefined, maxLocation)); + + DocumentManager.Instance.FollowLink(LinkFollowBox.linkDoc, LinkFollowBox.sourceDoc, focus, options && options.shouldZoom, false, undefined); + } + } + + @undoBatch + openLinkTab = () => { + if (LinkFollowBox.destinationDoc) { + const fullScreenAlias = Doc.MakeAlias(LinkFollowBox.destinationDoc); + // this.prosp.addDocTab is empty -- use the link source's addDocTab + (LinkFollowBox._addDocTab || this.props.addDocTab)(fullScreenAlias, undefined, "inTab"); + + this.highlightDoc(); + SelectionManager.DeselectAll(); + } + } + + @undoBatch + openLinkColTab = (options: { context: Doc, shouldZoom: boolean }) => { + if (LinkFollowBox.destinationDoc) { + options.context = Doc.IsPrototype(options.context) ? Doc.MakeDelegate(options.context) : options.context; + if (NumCast(options.context._viewType, CollectionViewType.Invalid) === CollectionViewType.Freeform) { + const newPanX = NumCast(LinkFollowBox.destinationDoc.x) + NumCast(LinkFollowBox.destinationDoc._width) / 2; + const newPanY = NumCast(LinkFollowBox.destinationDoc.y) + NumCast(LinkFollowBox.destinationDoc._height) / 2; + options.context._panX = newPanX; + options.context._panY = newPanY; + } + (LinkFollowBox._addDocTab || this.props.addDocTab)(options.context, undefined, "inTab"); + if (options.shouldZoom) this.jumpToLink({ shouldZoom: options.shouldZoom }); + + this.highlightDoc(); + SelectionManager.DeselectAll(); + } + } + + @undoBatch + openLinkInPlace = (options: { shouldZoom: boolean }) => { + + if (LinkFollowBox.destinationDoc && LinkFollowBox.sourceDoc) { + if (this.sourceView && this.sourceView.props.addDocument) { + const destViews = DocumentManager.Instance.getDocumentViews(LinkFollowBox.destinationDoc); + if (!destViews.find(dv => dv.props.ContainingCollectionView === this.sourceView!.props.ContainingCollectionView)) { + const alias = Doc.MakeAlias(LinkFollowBox.destinationDoc); + const y = NumCast(LinkFollowBox.sourceDoc.y); + const x = NumCast(LinkFollowBox.sourceDoc.x); + + const width = NumCast(LinkFollowBox.sourceDoc._width); + const height = NumCast(LinkFollowBox.sourceDoc._height); + + alias.x = x + width + 30; + alias.y = y; + alias._width = width; + alias._height = height; + + this.sourceView.props.addDocument(alias); + } + } + + this.jumpToLink({ shouldZoom: options.shouldZoom }); + + this.highlightDoc(); + SelectionManager.DeselectAll(); + } + } + + //set this to be the default link behavior, can be any of the above + public defaultLinkBehavior: (options?: any) => void = this.jumpToLink; + + @action + currentLinkBehavior = () => { + // this.resetPan(); + if (LinkFollowBox.destinationDoc) { + if (this.selectedContextString === "") { + this.selectedContextString = "self"; + this.selectedContext = LinkFollowBox.destinationDoc; + } + if (this.selectedOption === "") this.selectedOption = FollowOptions.NOZOOM; + const shouldZoom: boolean = this.selectedOption === FollowOptions.NOZOOM ? false : true; + const notOpenInContext: boolean = this.selectedContextString === "self" || this.selectedContextString === LinkFollowBox.destinationDoc[Id]; + + if (this.selectedMode === FollowModes.INPLACE) { + if (shouldZoom !== undefined) this.openLinkInPlace({ shouldZoom: shouldZoom }); + } + else if (this.selectedMode === FollowModes.OPENFULL) { + if (notOpenInContext) this.openFullScreen(); + else this.selectedContext && this.openColFullScreen({ context: this.selectedContext }); + } + else if (this.selectedMode === FollowModes.OPENRIGHT) { + if (notOpenInContext) this.openLinkRight(); + else this.selectedContext && this.openLinkColRight({ context: this.selectedContext, shouldZoom: shouldZoom }); + } + else if (this.selectedMode === FollowModes.OPENTAB) { + if (notOpenInContext) this.openLinkTab(); + else this.selectedContext && this.openLinkColTab({ context: this.selectedContext, shouldZoom: shouldZoom }); + } + else if (this.selectedMode === FollowModes.PAN) { + this.jumpToLink({ shouldZoom: shouldZoom }); + } + else return; + } + } + + @action + handleModeChange = (e: React.ChangeEvent) => { + const target = e.target as HTMLInputElement; + this.selectedMode = target.value; + this.selectedContext = undefined; + this.selectedContextString = ""; + + this.shouldUseOnlyParentContext = (this.selectedMode === FollowModes.INPLACE || this.selectedMode === FollowModes.PAN); + + if (this.shouldUseOnlyParentContext) { + if (this.sourceView && this.sourceView.props.ContainingCollectionDoc) { + this.selectedContext = this.sourceView.props.ContainingCollectionDoc; + this.selectedContextString = (StrCast(this.sourceView.props.ContainingCollectionDoc.title)); + } + } + } + + @action + handleOptionChange = (e: React.ChangeEvent) => { + const target = e.target as HTMLInputElement; + this.selectedOption = target.value; + } + + @action + handleContextChange = (e: React.ChangeEvent) => { + const target = e.target as HTMLInputElement; + this.selectedContextString = target.value; + // selectedContext is updated in reaction + this.selectedOption = ""; + } + + @computed + get canOpenInPlace() { + if (this.sourceView && this.sourceView.props.ContainingCollectionDoc) { + const colDoc = this.sourceView.props.ContainingCollectionDoc; + if (colDoc._viewType === CollectionViewType.Freeform) return true; + } + return false; + } + + @computed + get availableModes() { + return ( + <div> + <label><input + type="radio" + name="mode" + value={FollowModes.OPENRIGHT} + checked={this.selectedMode === FollowModes.OPENRIGHT} + onChange={this.handleModeChange} + disabled={false} /> + {FollowModes.OPENRIGHT} + </label><br /> + <label><input + type="radio" + name="mode" + value={FollowModes.OPENTAB} + checked={this.selectedMode === FollowModes.OPENTAB} + onChange={this.handleModeChange} + disabled={false} /> + {FollowModes.OPENTAB} + </label><br /> + <label><input + type="radio" + name="mode" + value={FollowModes.OPENFULL} + checked={this.selectedMode === FollowModes.OPENFULL} + onChange={this.handleModeChange} + disabled={false} /> + {FollowModes.OPENFULL} + </label><br /> + <label><input + type="radio" + name="mode" + value={FollowModes.PAN} + checked={this.selectedMode === FollowModes.PAN} + onChange={this.handleModeChange} + disabled={!this.canPan} /> + {FollowModes.PAN} + </label><br /> + <label><input + type="radio" + name="mode" + value={FollowModes.INPLACE} + checked={this.selectedMode === FollowModes.INPLACE} + onChange={this.handleModeChange} + disabled={!this.canOpenInPlace} /> + {FollowModes.INPLACE} + </label><br /> + </div> + ); + } + + @computed + get parentName() { + if (this.sourceView && this.sourceView.props.ContainingCollectionDoc) { + return this.sourceView.props.ContainingCollectionDoc.title; + } + } + + @computed + get parentID(): string { + if (this.sourceView && this.sourceView.props.ContainingCollectionDoc) { + return StrCast(this.sourceView.props.ContainingCollectionDoc[Id]); + } + return "col"; + } + + @computed + get availableContexts() { + return ( + this.shouldUseOnlyParentContext ? + <label><input + type="radio" disabled={true} + name="context" + value={this.parentID} + checked={true} /> + {this.parentName} (Parent Collection) + </label> + : + <div> + <label><input + type="radio" disabled={LinkFollowBox.linkDoc ? false : true} + name="context" + value={LinkFollowBox.destinationDoc ? StrCast(LinkFollowBox.destinationDoc[Id]) : "self"} + checked={LinkFollowBox.destinationDoc ? this.selectedContextString === StrCast(LinkFollowBox.destinationDoc[Id]) || this.selectedContextString === "self" : true} + onChange={this.handleContextChange} /> + Open Self + </label><br /> + {[...this._docs, ...this._otherDocs].map(doc => { + if (doc && doc.target && doc.col.title !== "Recently Closed") { + return <div key={doc.col[Id] + doc.target[Id]}><label key={doc.col[Id] + doc.target[Id]}> + <input + type="radio" disabled={LinkFollowBox.linkDoc ? false : true} + name="context" + value={StrCast(doc.col[Id])} + checked={this.selectedContextString === StrCast(doc.col[Id])} + onChange={this.handleContextChange} /> + {doc.col.title} + </label><br /></div>; + } + })} + </div> + ); + } + + @computed + get shouldShowZoom(): boolean { + if (this.selectedMode === FollowModes.OPENFULL) return false; + if (this.shouldUseOnlyParentContext) return true; + if (LinkFollowBox.destinationDoc ? this.selectedContextString === LinkFollowBox.destinationDoc[Id] : "self") return false; + + let contextMatch: boolean = false; + if (this.selectedContextAliases) { + this.selectedContextAliases.forEach(alias => { + if (alias._viewType === CollectionViewType.Freeform) contextMatch = true; + }); + } + if (contextMatch) return true; + + return false; + } + + @computed + get availableOptions() { + if (LinkFollowBox.destinationDoc) { + return ( + this.shouldShowZoom ? + <div> + <label><input + type="radio" + name="option" + value={FollowOptions.ZOOM} + checked={this.selectedOption === FollowOptions.ZOOM} + onChange={this.handleOptionChange} + disabled={false} /> + {FollowOptions.ZOOM} + </label><br /> + <label><input + type="radio" + name="option" + value={FollowOptions.NOZOOM} + checked={this.selectedOption === FollowOptions.NOZOOM} + onChange={this.handleOptionChange} + disabled={false} /> + {FollowOptions.NOZOOM} + </label><br /> + </div> + : + <div>No Available Options</div> + ); + } + return null; + } + + render() { + return ( + <div className="linkFollowBox-main" style={{ height: NumCast(this.props.Document._height), width: NumCast(this.props.Document._width) }}> + <div className="linkFollowBox-header"> + <div className="topHeader"> + {LinkFollowBox.linkDoc ? "Link Title: " + StrCast(LinkFollowBox.linkDoc.title) : "No Link Selected"} + <div onClick={() => this.props.Document.isMinimized = true} className="closeDocument"><FontAwesomeIcon icon={faTimes} size="lg" /></div> + </div> + <div className=" direction-indicator">{LinkFollowBox.linkDoc ? + LinkFollowBox.sourceDoc && LinkFollowBox.destinationDoc ? "Source: " + StrCast(LinkFollowBox.sourceDoc.title) + ", Destination: " + StrCast(LinkFollowBox.destinationDoc.title) + : "" : ""}</div> + </div> + <div className="linkFollowBox-content" style={{ height: NumCast(this.props.Document._height) - 110 }}> + <div className="linkFollowBox-item"> + <div className="linkFollowBox-item title">Mode</div> + <div className="linkFollowBox-itemContent"> + {LinkFollowBox.linkDoc ? this.availableModes : "Please select a link to view modes"} + </div> + </div> + <div className="linkFollowBox-item"> + <div className="linkFollowBox-item title">Context</div> + <div className="linkFollowBox-itemContent"> + {this.selectedMode !== "" ? this.availableContexts : "Please select a mode to view contexts"} + </div> + </div> + <div className="linkFollowBox-item"> + <div className="linkFollowBox-item title">Options</div> + <div className="linkFollowBox-itemContent"> + {this.selectedContextString !== "" ? this.availableOptions : "Please select a context to view options"} + </div> + </div> + </div> + <div className="linkFollowBox-footer"> + <button + onClick={this.resetVars}> + Clear Link + </button> + <div style={{ width: 20 }}></div> + <button + onClick={this.currentLinkBehavior} + disabled={(LinkFollowBox.linkDoc) ? false : true}> + Follow Link + </button> + </div> + </div> + ); + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 613929bca..feee1ffac 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -158,6 +158,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD this.layoutDoc._height = (this.layoutDoc._width || 0) / youtubeaspect; } } + + this.player && (this.player.style.transform = ""); } componentWillUnmount() { diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts index 337274774..2faf03517 100644 --- a/src/new_fields/Doc.ts +++ b/src/new_fields/Doc.ts @@ -254,6 +254,25 @@ export namespace Doc { // return Cast(field, ctor); // }); // } + + export function resetView(doc: Doc) { + doc._panX = doc._customOriginX ?? 0; + doc._panY = doc._customOriginY ?? 0; + doc.scale = doc._customOriginScale ?? 1; + } + + export function resetViewToOrigin(doc: Doc) { + doc._panX = 0; + doc._panY = 0; + doc.scale = 1; + } + + export function setView(doc: Doc) { + doc._customOriginX = doc._panX; + doc._customOriginY = doc._panY; + doc._customOriginScale = doc.scale; + } + export function RunCachedUpdate(doc: Doc, field: string) { const update = doc[CachedUpdates][field]; if (update) { @@ -561,6 +580,7 @@ export namespace Doc { copy[key] = Doc.MakeCopy(doc[key]!, false); } } else { + const field = ProxyField.WithoutProxy(() => doc[key]); if (field instanceof RefField) { copy[key] = field; } else if (cfield instanceof ComputedField) { diff --git a/src/new_fields/util.ts b/src/new_fields/util.ts index 8c719ccd8..740a77847 100644 --- a/src/new_fields/util.ts +++ b/src/new_fields/util.ts @@ -75,14 +75,15 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number } else { target.__fields[prop] = value; } - if (typeof value === "object" && !(value instanceof ObjectField)) debugger; + // if (typeof value === "object" && !(value instanceof ObjectField)) debugger; if (writeToServer) { if (value === undefined) target[Update]({ '$unset': { ["fields." + prop]: "" } }); else target[Update]({ '$set': { ["fields." + prop]: value instanceof ObjectField ? SerializationHelper.Serialize(value) : (value === undefined ? null : value) } }); } else { DocServer.registerDocWithCachedUpdate(receiver, prop as string, curValue); } - UndoManager.AddEvent({ + UndoManager. + AddEvent({ redo: () => receiver[prop] = value, undo: () => receiver[prop] = curValue }); |