diff --git a/dist/.gitignore b/dist/.gitignore index e60c27c89b..ef347b0d6c 100644 --- a/dist/.gitignore +++ b/dist/.gitignore @@ -27,4 +27,5 @@ nmh.log [0-9]*.js [0-9]*.js.map extension.crx -extension.pem \ No newline at end of file +extension.pem +extension.zip \ No newline at end of file diff --git a/dist/extension/js/foreground.js b/dist/extension/js/foreground.js index 9040aaa847..d84c901a42 100644 --- a/dist/extension/js/foreground.js +++ b/dist/extension/js/foreground.js @@ -1,6 +1,6 @@ (() => { - const extensionId = 'jkmhmgghdjjbafmkgjmplhemjjnkligf'; + const extensionId = 'jbnammhjiplhpjfncnlejjjejghimdkf'; const body = document.querySelector('body'); const container = document.createElement('div'); const dimmer = document.createElement('div'); diff --git a/electron/js/lib/installNativeMessagingHost.js b/electron/js/lib/installNativeMessagingHost.js index 6aff633229..71bf14333d 100644 --- a/electron/js/lib/installNativeMessagingHost.js +++ b/electron/js/lib/installNativeMessagingHost.js @@ -17,7 +17,7 @@ const { fixPathForAsarUnpack, is } = require('electron-util'); const APP_NAME = 'com.anytype.desktop'; const MANIFEST_FILENAME = `${APP_NAME}.json`; -const EXTENSION_ID = 'jkmhmgghdjjbafmkgjmplhemjjnkligf'; +const EXTENSION_ID = 'jbnammhjiplhpjfncnlejjjejghimdkf'; const USER_PATH = app.getPath('userData'); const EXE_PATH = app.getPath('exe'); diff --git a/electron/js/menu.js b/electron/js/menu.js index bef059b1e3..cbb2efb6d9 100644 --- a/electron/js/menu.js +++ b/electron/js/menu.js @@ -228,13 +228,6 @@ class MenuManager { Separator, - { - label: 'Experience gallery', - click: () => Util.send(this.win, 'popup', 'usecase', {}) - }, - - Separator, - { label: 'Export templates', click: () => Util.send(this.win, 'commandGlobal', 'exportTemplates') }, { label: 'Export objects', click: () => Util.send(this.win, 'commandGlobal', 'exportObjects') }, { label: 'Export localstore', click: () => Util.send(this.win, 'commandGlobal', 'exportLocalstore') }, diff --git a/electron/json/cors.json b/electron/json/cors.json index 87d2ebd1d8..47a090bdc2 100644 --- a/electron/json/cors.json +++ b/electron/json/cors.json @@ -119,7 +119,8 @@ "wss://*.biliapi.net", "https://sketchfab.com", "https://media.sketchfab.com", - "https://sentry.io" + "https://sentry.io", + "https://*.any.coop" ], "script-src-elem": [ diff --git a/src/img/arrow/usecaseCategory.svg b/src/img/arrow/usecaseCategory.svg new file mode 100644 index 0000000000..83f4df1bf9 --- /dev/null +++ b/src/img/arrow/usecaseCategory.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/img/icon/menu/space/gallery.svg b/src/img/icon/menu/space/gallery.svg new file mode 100644 index 0000000000..868bd72105 --- /dev/null +++ b/src/img/icon/menu/space/gallery.svg @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/img/icon/popup/usecase/any.svg b/src/img/icon/popup/usecase/any.svg new file mode 100644 index 0000000000..87f0e0cded --- /dev/null +++ b/src/img/icon/popup/usecase/any.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/img/icon/popup/usecase/heart.svg b/src/img/icon/popup/usecase/heart.svg new file mode 100644 index 0000000000..c2420c9f5e --- /dev/null +++ b/src/img/icon/popup/usecase/heart.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/json/extension.json b/src/json/extension.json index eb53f8448f..dc444e4048 100644 --- a/src/json/extension.json +++ b/src/json/extension.json @@ -1,6 +1,6 @@ { "clipper": { - "id": "jkmhmgghdjjbafmkgjmplhemjjnkligf", + "id": "jbnammhjiplhpjfncnlejjjejghimdkf", "name": "Anytype Webclipper", "prefix": "anytypeWebclipper", "emojiUrl": "https://anytype-static.fra1.cdn.digitaloceanspaces.com/emojies/" diff --git a/src/json/text.json b/src/json/text.json index d9b065d5e0..55c4e07f94 100644 --- a/src/json/text.json +++ b/src/json/text.json @@ -123,6 +123,7 @@ "commonSidebar": "Sidebar", "commonLanguage": "Language", "commonSpelling": "Spelling", + "commonGallery": "Gallery", "commonLCCollection": "Collection", "commonLCSet": "Set", @@ -336,6 +337,22 @@ "usecase6Title": "Empty", "usecase6Label": "Just a blank canvas", + "usecaseCategoryDashboard": "Dashboard", + "usecaseCategoryWork": "Work", + "usecaseCategoryEducation": "Education", + "usecaseCategoryPersonalGrowth": "Personal Growth", + "usecaseCategoryProjectTracking": "Project tracking", + "usecaseCategoryCollection": "Collection", + "usecaseCategoryHealthFitness": "Health & Fitness", + "usecaseCategoryFinance": "Finance", + "usecaseCategoryFoodNutrition": "Food & Nutrition", + "usecaseCategoryTravel": "Travel", + "usecaseCategoryHobbies": "Hobbies", + "usecaseCategoryHome": "Home", + "usecaseCategoryOther": "Other", + "usecaseCategoryFeatured": "Featured", + "usecaseCategoryMadeByAny": "Made by Any", + "pageMainEmptyDescription": "Select an Object to show when you login. You can always change it in Settings.", "pageMainNavigationItemEmptyTitle": "Object can not be shown", @@ -758,6 +775,58 @@ "popupSettingsOnboardingModeTitle": "Network", "popupSettingsOnboardingNetworkTitle": "Self-hosted Configuration", + "popupSettingsDataLocalFiles": "Local files", + "popupSettingsDataOffloadWarningTitle": "Are you sure?", + "popupSettingsDataOffloadWarningText": "All media files stored in Anytype will be deleted from your current device. They can be downloaded again from a backup node or another device.", + "popupSettingsDataFilesOffloaded": "Files offloaded", + + "popupSettingsDeleteTitle1": "1. You have 30 days to cancel account deletion", + "popupSettingsDeleteText1": "We're sorry to see you go. Once you request your account to be deleted, you have 30 days to cancel this request. After 30 days, your account data is permanently removed from the backup node, you won't be able to sign into Anytype on new devices.", + "popupSettingsDeleteTitle2": "2. Delete data from other devices", + "popupSettingsDeleteText2": "Since Anytype stores all your data locally on the device, you need to remove it from other devices also. Launch and remove data in Anytype or just delete the app.", + "popupSettingsDeleteCheckboxLabel": "I have read it and want to delete my account", + + "popupSettingsImportCsvTable": "Table", + "popupSettingsImportCsvCollection": "Collection", + "popupSettingsImportCsvMode": "Import CSV as", + "popupSettingsImportCsvUseFirstRow": "Use the first row as column names", + "popupSettingsImportCsvTranspose": "Transpose rows and columns", + "popupSettingsImportCsvColumnsDivider": "Columns are divided by", + "popupSettingsImportCsvCustomSymbol": "Custom symbol", + "popupSettingsImportCsvCustom": "Custom", + + "popupSettingsImportNotionHelpTitle": "How to import from Notion", + "popupSettingsImportNotionHelpStep": "Step %s", + "popupSettingsImportNotionHelpStep11": "Open Settings and members.", + "popupSettingsImportNotionHelpStep12": "Open My Connections and then Develop or manage integrations.", + "popupSettingsImportNotionHelpStep13": "Click New integration or Create new integration.", + "popupSettingsImportNotionHelpStep14": "Select your workspace and set Name for integration.", + "popupSettingsImportNotionHelpStep15": "Important! ️Go to Capabilities and select following capabilities and press save changes: ️", + "popupSettingsImportNotionHelpStep16": "Copy Internal Integration Secret for connecting and importing your data.", + "popupSettingsImportNotionHelpStep2Descr": "Add integration to the pages you want to import into Anytype. Page will be imported with all children documents. Select your root object to import all objects.", + "popupSettingsImportNotionHelpStep21": "Click on three dots on the upper right corner, then Add connections (you need to scroll the menu). Select your Anytype integration.", + "popupSettingsImportNotionHelpStep22": "Press Confirm. Now you just need to paste your Internal Integration Token to Anytype.", + + "popupSettingsImportNotionWarningTitle": "Some data formats will be imported as text", + "popupSettingsImportNotionWarningLi1": "All @mentions will be converted to text", + "popupSettingsImportNotionWarningLi2": "Date ranges will be imported as text", + "popupSettingsImportNotionWarningLi3": "Formulas and rollups will be placed as values", + "popupSettingsImportNotionWarningLi4": "Databases will look as Objects with Relations in Anytype documents", + "popupSettingsImportNotionWarningProceed": "Proceed", + + "popupSettingsImportNotionDescription": "Import your Notion files through the Notion API with 3 simple steps", + "popupSettingsImportNotionTokenPlaceholder": "Paste your integration token", + "popupSettingsImportNotionHowTo": "How to import from Notion", + "popupSettingsImportNotionStepByStepGuide": "Step-by-step guide", + "popupSettingsImportNotionIntegrationList11": "Create the integration you need to get Notion files", + "popupSettingsImportNotionIntegrationList12": "Settings & members → My connections → Develop or manage integrations → New integration", + "popupSettingsImportNotionIntegrationList21": "Provide read user information capability to integration", + "popupSettingsImportNotionIntegrationList22": "Integration settings → Capabilities → Read user information without email addresses", + "popupSettingsImportNotionIntegrationList31": "Add integration to the pages you want to import", + "popupSettingsImportNotionIntegrationList32": "Select document → ... → Add Connections → Confirm Integration", + + "popupSettingsPinCheckTimeOut": "PIN code check time-out", + "popupInviteRequestTitle": "Join a space", "popupInviteRequestText": "You've been invited to join %s space, created by %s. Send a request so space owner can let you in.", "popupInviteRequestMessagePlaceholder": "Leave a private comment for a space owner", @@ -830,58 +899,6 @@ "popupIndexComponentNotFound": "Component %s not found", - "popupSettingsDataLocalFiles": "Local files", - "popupSettingsDataOffloadWarningTitle": "Are you sure?", - "popupSettingsDataOffloadWarningText": "All media files stored in Anytype will be deleted from your current device. They can be downloaded again from a backup node or another device.", - "popupSettingsDataFilesOffloaded": "Files offloaded", - - "popupSettingsDeleteTitle1": "1. You have 30 days to cancel account deletion", - "popupSettingsDeleteText1": "We're sorry to see you go. Once you request your account to be deleted, you have 30 days to cancel this request. After 30 days, your account data is permanently removed from the backup node, you won't be able to sign into Anytype on new devices.", - "popupSettingsDeleteTitle2": "2. Delete data from other devices", - "popupSettingsDeleteText2": "Since Anytype stores all your data locally on the device, you need to remove it from other devices also. Launch and remove data in Anytype or just delete the app.", - "popupSettingsDeleteCheckboxLabel": "I have read it and want to delete my account", - - "popupSettingsImportCsvTable": "Table", - "popupSettingsImportCsvCollection": "Collection", - "popupSettingsImportCsvMode": "Import CSV as", - "popupSettingsImportCsvUseFirstRow": "Use the first row as column names", - "popupSettingsImportCsvTranspose": "Transpose rows and columns", - "popupSettingsImportCsvColumnsDivider": "Columns are divided by", - "popupSettingsImportCsvCustomSymbol": "Custom symbol", - "popupSettingsImportCsvCustom": "Custom", - - "popupSettingsImportNotionHelpTitle": "How to import from Notion", - "popupSettingsImportNotionHelpStep": "Step %s", - "popupSettingsImportNotionHelpStep11": "Open Settings and members.", - "popupSettingsImportNotionHelpStep12": "Open My Connections and then Develop or manage integrations.", - "popupSettingsImportNotionHelpStep13": "Click New integration or Create new integration.", - "popupSettingsImportNotionHelpStep14": "Select your workspace and set Name for integration.", - "popupSettingsImportNotionHelpStep15": "Important! ️Go to Capabilities and select following capabilities and press save changes: ️", - "popupSettingsImportNotionHelpStep16": "Copy Internal Integration Secret for connecting and importing your data.", - "popupSettingsImportNotionHelpStep2Descr": "Add integration to the pages you want to import into Anytype. Page will be imported with all children documents. Select your root object to import all objects.", - "popupSettingsImportNotionHelpStep21": "Click on three dots on the upper right corner, then Add connections (you need to scroll the menu). Select your Anytype integration.", - "popupSettingsImportNotionHelpStep22": "Press Confirm. Now you just need to paste your Internal Integration Token to Anytype.", - - "popupSettingsImportNotionWarningTitle": "Some data formats will be imported as text", - "popupSettingsImportNotionWarningLi1": "All @mentions will be converted to text", - "popupSettingsImportNotionWarningLi2": "Date ranges will be imported as text", - "popupSettingsImportNotionWarningLi3": "Formulas and rollups will be placed as values", - "popupSettingsImportNotionWarningLi4": "Databases will look as Objects with Relations in Anytype documents", - "popupSettingsImportNotionWarningProceed": "Proceed", - - "popupSettingsImportNotionDescription": "Import your Notion files through the Notion API with 3 simple steps", - "popupSettingsImportNotionTokenPlaceholder": "Paste your integration token", - "popupSettingsImportNotionHowTo": "How to import from Notion", - "popupSettingsImportNotionStepByStepGuide": "Step-by-step guide", - "popupSettingsImportNotionIntegrationList11": "Create the integration you need to get Notion files", - "popupSettingsImportNotionIntegrationList12": "Settings & members → My connections → Develop or manage integrations → New integration", - "popupSettingsImportNotionIntegrationList21": "Provide read user information capability to integration", - "popupSettingsImportNotionIntegrationList22": "Integration settings → Capabilities → Read user information without email addresses", - "popupSettingsImportNotionIntegrationList31": "Add integration to the pages you want to import", - "popupSettingsImportNotionIntegrationList32": "Select document → ... → Add Connections → Confirm Integration", - - "popupSettingsPinCheckTimeOut": "PIN code check time-out", - "popupShortcutMain": "Main", "popupShortcutNavigation": "Navigation", "popupShortcutMarkdown": "Markdown", @@ -1040,6 +1057,10 @@ "popupUsecaseInstall": "Install", "popupUsecaseMenuLabel": "Install experience to", "popupUsecaseSpaceCreate": "Create new space", + "popupUsecaseListTitle": "ANY Experiences gallery", + "popupUsecaseListText": "Explore experiences made by power users.
Simply install to your space and boost up your workflow.", + "popupUsecaseListEmptyCategory": "No Experiences found in the %s category", + "popupUsecaseListEmptyFilter": "There are no Experiences named \"%s\"<\/b>", "popupRelationValueRemoveTitle": "Are you sure?", "popupRelationValueRemoveText": "Deleting relation options can affect your objects. Once confirmed, the action cannot be undone.", diff --git a/src/scss/_vars.scss b/src/scss/_vars.scss index 0d2063f656..42d541907e 100644 --- a/src/scss/_vars.scss +++ b/src/scss/_vars.scss @@ -14,6 +14,7 @@ --color-shape-highlight-medium: rgba(79, 79, 79, 0.08); --color-shape-highlight-light: rgba(79, 79, 79, 0.04); + --color-shape-highlight-light-solid: #f8f8f8; /* Control */ diff --git a/src/scss/menu/space.scss b/src/scss/menu/space.scss index 461a163be5..dd8bf1a883 100644 --- a/src/scss/menu/space.scss +++ b/src/scss/menu/space.scss @@ -30,17 +30,20 @@ display: flex; flex-direction: column; align-items: center; gap: 8px 0px; } .item { - .iconWrap { width: 96px; height: 96px; border-radius: 4px; overflow: hidden; } + .iconWrap { width: 96px; height: 96px; border-radius: 4px; overflow: hidden; background-position: center; background-repeat: no-repeat; } .name { @include text-overflow-nw; @include text-small; width: 100%; text-align: center; font-weight: 500; } } .item.add { .iconWrap { - background-image: url('~img/icon/plus/space.svg'); background-size: 20px; - background-position: center; background-repeat: no-repeat; box-shadow: 0px 0px 0px 1px var(--color-control-active) inset; + background-image: url('~img/icon/plus/space.svg'); background-size: 20px; box-shadow: 0px 0px 0px 1px var(--color-control-active) inset; } } + .item.gallery { + .iconWrap { background-image: url('~img/icon/menu/space/gallery.svg'); } + } + .item.hover { background: rgba(37, 37, 37, 0.15); } } } diff --git a/src/scss/popup/settings.scss b/src/scss/popup/settings.scss index b4fc825534..600c5231a4 100644 --- a/src/scss/popup/settings.scss +++ b/src/scss/popup/settings.scss @@ -68,12 +68,9 @@ > .side.right.isFull { width: 100%; } > .side.right.isSubPage { padding-top: 0px; } > .side.right { - .head { - width: 100%; margin: 0px 0px 14px 0px; position: sticky; top: 0px; left: 0px; z-index: 10; - } + .head { width: 100%; margin: 0px 0px 14px 0px; position: sticky; top: 0px; left: 0px; z-index: 10; } .head { .inner { display: flex; flex-direction: row; align-items: center; background: var(--color-bg-primary); padding: 22px 0px; } - .icon.back { width: 8px; height: 8px; background-image: url('~img/icon/popup/settings/back.svg'); } .element { diff --git a/src/scss/popup/usecase.scss b/src/scss/popup/usecase.scss index 51ab148f2b..d7b674ec2a 100644 --- a/src/scss/popup/usecase.scss +++ b/src/scss/popup/usecase.scss @@ -1,26 +1,113 @@ @import "~scss/_vars"; +$shadow: 0px 2px 20px rgba(0,0,0,0.2); + .popups { .popup.popupUsecase { - .innerWrap { width: 816px; max-height: calc(100% - 128px); } + .content { height: 100%; } + .innerWrap { width: 960px; height: 772px; max-height: calc(100% - 128px); } - .page { padding: 32px 56px; } + @media (max-width: 896px) { + .innerWrap { width: calc(100% - 64px); left: 16px; margin-left: 0px !important; } + } + + .emptySearch { + color: var(--color-text-secondary); display: flex; align-items: center; justify-content: center; border-bottom: 1px solid var(--color-shape-secondary); + border-top: 1px solid var(--color-shape-secondary); text-align: center; padding: 16px 0px; height: 300px; margin: 56px 0px 0px 0px; + } + .emptySearch { + b { display: inline; } + .inner { width: 300px; } + } .page.pageList { - .items { display: grid; gap: 32px; grid-template-columns: repeat(2, minmax(0, 1fr)); } - .item { - .picture { - aspect-ratio: 16/9; background-size: cover; background-repeat: no-repeat; background-position: center; border-radius: 8px; - margin: 0px 0px 12px 0px; + .mid { padding: 56px 56px 0px 56px; text-align: center; } + .mid { + .title { margin: 0px 0px 2px 0px; } + .label { margin: 0px 0px 16px 0px; } + + .filter { text-align: left; max-width: 302px; height: 36px; margin: 0px auto; border: 1px solid var(--color-shape-primary); border-radius: 7px; padding: 0px; } + .filter { + .line { display: none; } + .inner { height: 100%; text-align: left; padding: 0px 10px; justify-content: center; } + } + } + + .categories { + padding: 18px 24px; background: var(--color-shape-highlight-light-solid); border-bottom: 1px solid var(--color-shape-secondary); + position: sticky; top: 0px; left: 0px; z-index: 1; + } + .categories { + .inner { transform: translate3d(0px,0px,0px); transition: 0.3s transform $easeInQuint; white-space: nowrap; } + .item { + white-space: nowrap; padding: 2px 12px; @include text-common; border: 1px solid var(--color-control-accent); border-radius: 14px; + background: var(--color-bg-primary); transition: $transitionAllCommon; display: inline-flex; flex-direction: row; align-items: center; gap: 0px 8px; + margin: 0px 8px 0px 0px; vertical-align: top;; + } + .item:hover, .item.active { background: var(--color-shape-highlight-medium); } + .item:last-child { margin: 0px; } + + .div { width: 1px; height: 28px; background: var(--color-control-active); display: inline-block; vertical-align: top; margin: 0px 8px 0px 0px; } + + .item { + .icon { width: 16px; height: 16px; background-size: contain; } + .icon.heart { background-image: url('~img/icon/popup/usecase/heart.svg'); } + .icon.any { background-image: url('~img/icon/popup/usecase/any.svg'); } + } + + .gradient { position: absolute; top: 0px; z-index: 1; width: 26%; height: 100%; pointer-events: none; transition: $transitionAllCommon; } + .gradient.left { left: 0px; background: linear-gradient(to left, rgba(79, 79, 79, 0) 0%, var(--color-shape-highlight-light-solid) 100%); } + .gradient.right { right: 0px; background: linear-gradient(to right, rgba(79, 79, 79, 0) 0%, var(--color-shape-highlight-light-solid) 100%); } + + .icon.arrow { + position: absolute; top: 50%; width: 32px; height: 32px; background-color: var(--color-bg-primary); border-radius: 50%; + z-index: 2; margin-top: -16px; background-image: url('~img/arrow/usecaseCategory.svg'); + background-size: 20px; background-position: center; background-repeat: no-repeat; transition: $transitionAllCommon; + } + .icon.arrow:hover { background-color: var(--color-shape-secondary); } + .icon.arrow.left { left: 16px; transform: rotateZ(180deg); box-shadow: 0px -2px 8px rgba(0,0,0,0.2); } + .icon.arrow.right { right: 16px; box-shadow: 0px 2px 8px rgba(0,0,0,0.2); } + } + + .items { padding: 0px 56px; } + .items { + .ReactVirtualized__List { padding: 56px 0px 24px 0px; overflow: visible !important; } + .ReactVirtualized__Grid__innerScrollContainer { overflow: visible !important; } + + .row { display: grid; gap: 32px; grid-template-columns: repeat(2, minmax(0, 1fr)); } + + .item { margin: 0px 0px 32px 0px; } + .item { + .picture { + aspect-ratio: 68/45; background-size: cover; background-repeat: no-repeat; background-position: center; border-radius: 8px; + margin: 0px 0px 12px 0px; box-shadow: $shadow; + } + .name { @include text-header2; @include clamp1; margin: 0px 0px 2px 0px; font-weight: 500; } + .descr { @include text-common; @include clamp2; margin: 0px 0px 12px 0px; } + .author { @include text-small; color: var(--color-text-secondary); } } - .name { @include text-paragraph; @include clamp1; margin: 0px 0px 4px 0px; font-weight: 600; } - .descr { @include text-small; @include clamp2; } } } + .page.pageItem { padding: 0px 56px 32px 56px; } .page.pageItem { + .head { + width: 100%; margin: 0px 0px 20px 0px; padding: 22px 0px 0px 0px; background: var(--color-bg-primary); position: sticky; + left: 0px; top: 0px; z-index: 1; + } + .head { + .inner { display: flex; flex-direction: row; align-items: center; background: var(--color-bg-primary); } + .icon.back { width: 8px; height: 8px; background-image: url('~img/icon/popup/settings/back.svg'); } + + .element { + color: var(--color-text-secondary); display: inline-flex; gap: 0px 6px; align-items: center; padding: 2px 8px 2px 6px; + border-radius: 6px; margin-left: -6px; + } + .element:hover { background: var(--color-shape-highlight-medium); } + } + .titleWrap { - margin: 0px 0px 40px 0px; display: flex; flex-direction: row; align-items: center; justify-content: stretch; gap: 0px 16px; + margin: 0px 0px 22px 0px; display: flex; flex-direction: row; align-items: center; justify-content: stretch; gap: 0px 16px; } .titleWrap { .side.left { flex-grow: 1; display: flex; flex-direction: column; gap: 8px 0px; } @@ -36,12 +123,8 @@ } .screenWrap { .swiper { height: 100%; overflow: visible; } - .swiper-slide { display: flex; align-items: center; justify-content: center; } - - .screen { - width: 100%; box-shadow: 0px 2px 28px 0px rgba(0, 0, 0, 0.20); background-size: contain; - background-position: center; background-repeat: no-repeat; border-radius: 6px; background-color: var(--color-bg-primary); - } + .swiper-slide { display: flex; align-items: center; justify-content: center; overflow: hidden; border-radius: 8px; box-shadow: $shadow; } + .screen { width: 100%; height: 100%; object-fit: cover; } .icon.arrow { position: absolute; width: 44px; height: 44px; top: 50%; margin-top: -22px; background-color: var(--color-bg-primary); z-index: 1; @@ -49,8 +132,8 @@ transition: $transitionAllCommon; border: 1px solid var(--color-shape-primary); } .icon.arrow.hide { opacity: 0; } - .icon.arrow.left { left: 24px; transform: rotateZ(180deg); } - .icon.arrow.right { right: 24px; } + .icon.arrow.left { left: 16px; transform: rotateZ(180deg); } + .icon.arrow.right { right: 16px; } } .footerWrap { display: flex; flex-direction: row; align-items: flex-start; justify-content: stretch; } diff --git a/src/ts/component/block/dataview/view/gallery.tsx b/src/ts/component/block/dataview/view/gallery.tsx index ced2aac679..4441563664 100644 --- a/src/ts/component/block/dataview/view/gallery.tsx +++ b/src/ts/component/block/dataview/view/gallery.tsx @@ -107,7 +107,7 @@ const ViewGallery = observer(class ViewGallery extends React.Component {({ measure }) => ( -
+
{item.children.map(id => row(id))}
)} diff --git a/src/ts/component/drag/provider.tsx b/src/ts/component/drag/provider.tsx index 42c30a8e3b..e4b94c2269 100644 --- a/src/ts/component/drag/provider.tsx +++ b/src/ts/component/drag/provider.tsx @@ -141,7 +141,7 @@ const DragProvider = observer(class DragProvider extends React.Component const dataTransfer = e.dataTransfer; const items = UtilCommon.getDataTransferItems(dataTransfer.items); const isFileDrop = dataTransfer.files && dataTransfer.files.length; - const last = blockStore.getFirstBlock(rootId, -1, it => it.canCreateBlock()); + const last = blockStore.getFirstBlock(rootId, -1, it => it && it.canCreateBlock()); let position = this.position; let data: any = null; diff --git a/src/ts/component/list/objectManager.tsx b/src/ts/component/list/objectManager.tsx index 3985a86d7a..0d86a4b3a1 100644 --- a/src/ts/component/list/objectManager.tsx +++ b/src/ts/component/list/objectManager.tsx @@ -179,9 +179,7 @@ const ListObjectManager = observer(class ListObjectManager extends React.Compone textEmpty = UtilCommon.sprintf(translate('popupSearchNoObjects'), filter); }; - content = ( - - ); + content = ; } else { content = (
diff --git a/src/ts/component/menu/space.tsx b/src/ts/component/menu/space.tsx index 468b01084b..e7583c5095 100644 --- a/src/ts/component/menu/space.tsx +++ b/src/ts/component/menu/space.tsx @@ -39,7 +39,7 @@ const MenuSpace = observer(class MenuSpace extends React.Component { className={cn.join(' ')} onClick={e => this.onClick(e, item)} onMouseEnter={e => this.onMouseEnter(e, item)} - onMouseLeave={e => setHover()} + onMouseLeave={() => setHover()} onContextMenu={e => this.onContextMenu(e, item)} >
@@ -50,16 +50,16 @@ const MenuSpace = observer(class MenuSpace extends React.Component { ); }; - const ItemAdd = (item: any) => ( + const ItemIcon = (item: any) => (
this.onClick(e, item)} onMouseEnter={e => this.onMouseEnter(e, item)} onMouseLeave={e => setHover()} >
-
{translate('commonCreateNew')}
+
{item.name}
); @@ -79,8 +79,8 @@ const MenuSpace = observer(class MenuSpace extends React.Component {
{items.map(item => { - if (item.id == 'add') { - return ; + if ([ 'add', 'gallery' ].includes(item.id)) { + return ; } else { return ; }; @@ -205,14 +205,20 @@ const MenuSpace = observer(class MenuSpace extends React.Component { const items = UtilCommon.objectCopy(dbStore.getSpaces()); if (items.length < Constant.limit.space) { - items.push({ id: 'add' }); + items.push({ id: 'add', name: translate('commonCreateNew') }); }; + + items.push({ id: 'gallery', name: translate('commonGallery') }); + return items; }; onClick (e: any, item: any) { if (item.id == 'add') { this.onAdd(); + } else + if (item.id == 'gallery') { + popupStore.open('usecase', {}); } else { UtilRouter.switchSpace(item.targetSpaceId); analytics.event('SwitchSpace'); diff --git a/src/ts/component/popup/page/usecase/item.tsx b/src/ts/component/popup/page/usecase/item.tsx index 314ddf3895..8f19d8a5f3 100644 --- a/src/ts/component/popup/page/usecase/item.tsx +++ b/src/ts/component/popup/page/usecase/item.tsx @@ -25,14 +25,14 @@ class PopupUsecasePageItem extends React.Component { super(props); this.onMenu = this.onMenu.bind(this); - this.onAuthor = this.onAuthor.bind(this); this.onSwiper = this.onSwiper.bind(this); }; render () { + const { getAuthor, onAuthor, onPage } = this.props; const { isLoading, error } = this.state; const object = this.getObject(); - const author = this.getAuthor(); + const author = getAuthor(object.author); const screenshots = object.screenshots || []; const categories = (object.categories || []).slice(0, 10); @@ -40,10 +40,19 @@ class PopupUsecasePageItem extends React.Component {
this.node = ref}> {isLoading ? : ''} +
+
+
onPage('', {})}> + + {translate('commonBack')} +
+
+
+
- <Label text={UtilCommon.sprintf(translate('popupUsecaseAuthor'), author)} onClick={this.onAuthor} /> + <Label text={UtilCommon.sprintf(translate('popupUsecaseAuthor'), author)} onClick={() => onAuthor(object.author)} /> </div> <div className="side right"> <Button ref={ref => this.refButton = ref} id="button-install" text={translate('popupUsecaseInstall')} arrow={true} onClick={this.onMenu} /> @@ -55,7 +64,7 @@ class PopupUsecasePageItem extends React.Component<I.PopupUsecase, State> { <div className="screenWrap"> <Swiper spaceBetween={20} - slidesPerView={1} + slidesPerView={1.5} onSlideChange={() => this.checkArrows()} onSwiper={swiper => this.onSwiper(swiper)} > @@ -169,14 +178,6 @@ class PopupUsecasePageItem extends React.Component<I.PopupUsecase, State> { return list; }; - onAuthor () { - const object = this.getObject(); - - if (object.author) { - Renderer.send('urlOpen', object.author); - }; - }; - getObject (): any { const { param } = this.props; const { data } = param; @@ -184,19 +185,6 @@ class PopupUsecasePageItem extends React.Component<I.PopupUsecase, State> { return data.object || {}; }; - getAuthor (): string { - const object = this.getObject(); - - if (!object.author) { - return ''; - }; - - let a: any = {}; - try { a = new URL(object.author); } catch (e) {}; - - return String(a.pathname || '').replace(/^\//, ''); - }; - }; export default PopupUsecasePageItem; \ No newline at end of file diff --git a/src/ts/component/popup/page/usecase/list.tsx b/src/ts/component/popup/page/usecase/list.tsx index 98891274b3..1be037fb54 100644 --- a/src/ts/component/popup/page/usecase/list.tsx +++ b/src/ts/component/popup/page/usecase/list.tsx @@ -1,22 +1,75 @@ import * as React from 'react'; -import { I, C } from 'Lib'; +import { Loader, Title, Label, EmptySearch, Icon, Filter } from 'Component'; +import { I, C, translate, UtilCommon } from 'Lib'; import { AutoSizer, CellMeasurer, CellMeasurerCache, List, WindowScroller } from 'react-virtualized'; +import { commonStore } from 'Store'; + +interface State { + isLoading: boolean; + category: any; +}; const HEIGHT = 450; +const LIMIT = 2; -class PopupUsecasePageList extends React.Component<I.PopupUsecase> { +class PopupUsecasePageList extends React.Component<I.PopupUsecase, State> { node = null; refList = null; - list: any = []; - categories: any = []; + refFilter = null; cache: any = {}; + timeoutResize = 0; + timeoutFilter = 0; + pages = 0; + page = 0; + state = { + isLoading: false, + category: null, + }; constructor (props: I.PopupUsecase) { super(props); + + this.cache = new CellMeasurerCache({ + defaultHeight: HEIGHT, + fixedWidth: true, + }); + + this.onResize = this.onResize.bind(this); + this.onCategory = this.onCategory.bind(this); + this.onFilterChange = this.onFilterChange.bind(this); + this.onFilterClear = this.onFilterClear.bind(this); }; render () { + const { getAuthor, onAuthor } = this.props; + const { isLoading, category } = this.state; + const items = this.getItems(); + const { gallery } = commonStore; + const filter = this.refFilter ? this.refFilter.getValue() : ''; + + if (isLoading) { + return <Loader id="loader" />; + }; + + let textEmpty = ''; + if (filter) { + textEmpty = UtilCommon.sprintf(translate('popupUsecaseListEmptyFilter'), filter); + } else + if (category) { + textEmpty = UtilCommon.sprintf(translate('popupUsecaseListEmptyCategory'), category.name); + }; + + const Category = (item: any) => ( + <div + className={[ 'item', (category && (category?.id == item.id)) ? 'active' : '' ].join(' ')} + onClick={() => this.onCategory(item)} + > + {item.icon ? <Icon className={item.icon} /> : ''} + {item.name} + </div> + ); + const Item = (item: any) => { const screenshot = item.screenshots.length ? item.screenshots[0] : ''; @@ -25,12 +78,14 @@ class PopupUsecasePageList extends React.Component<I.PopupUsecase> { <div className="picture" style={{ backgroundImage: `url("${screenshot}")` }}></div> <div className="name">{item.title}</div> <div className="descr">{item.description}</div> + <div className="author" onClick={() => onAuthor(item.author)}>@{getAuthor(item.author)}</div> </div> ); }; const rowRenderer = (param: any) => { - const item: any = this.list[param.index]; + const item: any = items[param.index]; + return ( <CellMeasurer key={param.key} @@ -40,64 +95,229 @@ class PopupUsecasePageList extends React.Component<I.PopupUsecase> { rowIndex={param.index} hasFixedWidth={() => {}} > - <Item key={item.id} {...item} index={param.index} style={param.style} /> + {({ measure }) => ( + <div key={`gallery-row-${param.index}`} className="row" style={param.style}> + {item.children.map(child => <Item key={child.id} {...child} />)} + </div> + )} </CellMeasurer> ); }; return ( - <div ref={ref => this.node = ref}> + <div ref={ref => this.node = ref} className="wrap"> + <div id="categories" className="categories"> + <div id="inner" className="inner"> + {gallery.categories.map((item: any, i: number) => ( + <React.Fragment key={i}> + <Category {...item} /> + {item.id == 'made-by-any' ? <div className="div" /> : ''} + </React.Fragment> + ))} + </div> + + <div id="gradientLeft" className="gradient left" /> + <div id="gradientRight" className="gradient right" /> + + <Icon id="arrowLeft" className="arrow left" onClick={() => this.onArrow(-1)} /> + <Icon id="arrowRight" className="arrow right" onClick={() => this.onArrow(1)} /> + </div> + + <div className="mid"> + <Title text={translate('popupUsecaseListTitle')} /> + <Label text={translate('popupUsecaseListText')} /> + + <Filter + ref={ref => this.refFilter = ref} + id="store-filter" + icon="search" + placeholder={translate('commonSearchPlaceholder')} + onChange={this.onFilterChange} + onClear={this.onFilterClear} + /> + </div> + <div className="items"> - {this.list.map((item: any) => ( - <Item key={item.id} {...item} /> - ))} - <WindowScroller scrollElement={$('#popupUsecase-innerWrap').get(0)}> - {({ height, isScrolling, registerChild, scrollTop }) => ( - <AutoSizer disableHeight={true} className="scrollArea"> - {({ width }) => ( - <List - ref={ref => this.refList = ref} - autoHeight={true} - height={Number(height) || 0} - width={Number(width) || 0} - deferredMeasurmentCache={this.cache} - rowCount={length} - rowHeight={HEIGHT} - rowRenderer={rowRenderer} - isScrolling={isScrolling} - scrollTop={scrollTop} - /> - )} - </AutoSizer> - )} - </WindowScroller> + {!items.length ? ( + <EmptySearch text={textEmpty} /> + ) : ( + <WindowScroller scrollElement={$('#popupUsecase-innerWrap').get(0)}> + {({ height, isScrolling, registerChild, scrollTop }) => ( + <AutoSizer disableHeight={true} className="scrollArea" onResize={this.onResize}> + {({ width }) => ( + <List + ref={ref => this.refList = ref} + autoHeight={true} + height={Number(height) || 0} + width={Number(width) || 0} + deferredMeasurmentCache={this.cache} + rowCount={items.length} + rowHeight={param => this.cache.rowHeight(param)} + rowRenderer={rowRenderer} + isScrolling={isScrolling} + scrollTop={scrollTop} + /> + )} + </AutoSizer> + )} + </WindowScroller> + )} </div> </div> ); }; componentDidMount (): void { + if (commonStore.gallery.list.length) { + return; + }; + + this.setState({ isLoading: true }); + C.GalleryDownloadIndex((message: any) => { - this.categories = message.categories; - this.list = message.list; - this.forceUpdate(); + commonStore.gallery = { + categories: (message.categories || []).map(it => ({ ...it, name: this.categoryName(it.id) })), + list: message.list || [], + }; + + this.setState({ isLoading: false }); }); }; componentDidUpdate (): void { - this.cache = new CellMeasurerCache({ - fixedWidth: true, - defaultHeight: HEIGHT, - keyMapper: i => this.list[i].id, - }); + this.reset(); + this.checkPage(); + this.props.position(); + }; - this.props.position(); + componentWillUnmount(): void { + window.clearTimeout(this.timeoutResize); + window.clearTimeout(this.timeoutFilter); + }; + + reset () { + this.cache.clearAll(); + + if (this.refList) { + this.refList.recomputeRowHeights(0); + }; }; onClick (e: any, item: any) { this.props.onPage('item', { object: item }); }; + onCategory (item: any) { + this.setState({ category: item }); + }; + + onFilterChange (v: string) { + window.clearTimeout(this.timeoutFilter); + this.timeoutFilter = window.setTimeout(() => this.forceUpdate(), 500); + }; + + onFilterClear () { + this.forceUpdate(); + }; + + getItems () { + const { category } = this.state; + const ret: any[] = []; + const filter = this.refFilter ? this.refFilter.getValue() : ''; + + let items = commonStore.gallery.list || []; + if (category) { + items = items.filter(it => category.list.includes(it.name)); + }; + + if (filter) { + const reg = new RegExp(UtilCommon.regexEscape(filter), 'gi'); + items = items.filter(it => reg.test(it.title) || reg.test(it.description)); + }; + + let n = 0; + let row = { children: [] }; + + for (const item of items) { + row.children.push(item); + + n++; + if (n == LIMIT) { + ret.push(row); + row = { children: [] }; + n = 0; + }; + }; + + if (row.children.length < LIMIT) { + ret.push(row); + }; + + return ret.filter(it => it.children.length > 0); + }; + + onResize ({ width }) { + window.clearTimeout(this.timeoutResize); + this.timeoutResize = window.setTimeout(() => this.forceUpdate(), 10); + }; + + categoryName (id: string) { + return translate(UtilCommon.toCamelCase(`usecaseCategory-${id}`)); + }; + + calcPages () { + const { categories } = commonStore.gallery; + const node = $(this.node); + const width = node.width(); + const items = node.find('#categories .item'); + + let iw = 0; + items.each((i, item) => { + iw += $(item).outerWidth(true); + }); + iw += 8 * categories.length + 1; + + this.pages = Number(Math.ceil(iw / width)) || 1; + }; + + checkPage () { + const node = $(this.node); + const gradientLeft = node.find('#gradientLeft'); + const gradientRight = node.find('#gradientRight'); + const arrowLeft = node.find('#arrowLeft'); + const arrowRight = node.find('#arrowRight'); + + this.calcPages(); + this.page = Math.max(0, this.page); + this.page = Math.min(this.page, this.pages - 1); + + if (!this.page) { + gradientLeft.hide(); + arrowLeft.hide(); + } else { + gradientLeft.show(); + arrowLeft.show(); + }; + + if (this.page == this.pages - 1) { + gradientRight.hide(); + arrowRight.hide(); + } else { + gradientRight.show(); + arrowRight.show(); + }; + }; + + onArrow (dir: number) { + const node = $(this.node); + const inner = node.find('#categories #inner'); + + this.page += dir; + this.checkPage(); + + inner.css({ transform: `translate3d(${-this.page * 100}%, 0px, 0px)` }); + }; + }; export default PopupUsecasePageList; \ No newline at end of file diff --git a/src/ts/component/popup/usecase.tsx b/src/ts/component/popup/usecase.tsx index 77702a8576..20d3e9562a 100644 --- a/src/ts/component/popup/usecase.tsx +++ b/src/ts/component/popup/usecase.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { observer } from 'mobx-react'; -import { I, UtilCommon } from 'Lib'; +import { I, UtilCommon, Renderer } from 'Lib'; import PopupUsecasePageList from './page/usecase/list'; import PopupUsecasePageItem from './page/usecase/item'; @@ -19,6 +19,8 @@ const PopupUsecase = observer(class PopupUsecase extends React.Component<I.Popup super(props); this.onPage = this.onPage.bind(this); + this.getAuthor = this.getAuthor.bind(this); + this.onAuthor = this.onAuthor.bind(this); }; render () { @@ -34,6 +36,8 @@ const PopupUsecase = observer(class PopupUsecase extends React.Component<I.Popup ref={ref => this.ref = ref} {...this.props} onPage={this.onPage} + getAuthor={this.getAuthor} + onAuthor={this.onAuthor} /> ); }; @@ -57,8 +61,30 @@ const PopupUsecase = observer(class PopupUsecase extends React.Component<I.Popup }; onPage (page: string, data?: any): void { + const { param, getId } = this.props; + const obj = $(`#${getId()}-innerWrap`); + data = data || {}; - this.props.param.data = Object.assign(this.props.param.data, { ...data, page }); + param.data = Object.assign(param.data, { ...data, page }); + + obj.scrollTop(0); + }; + + getAuthor (author: string): string { + if (!author) { + return ''; + }; + + let a: any = {}; + try { a = new URL(author); } catch (e) {}; + + return String(a.pathname || '').replace(/^\//, ''); + }; + + onAuthor (author: string): void { + if (author) { + Renderer.send('urlOpen', author); + }; }; }); diff --git a/src/ts/interface/popup.ts b/src/ts/interface/popup.ts index bd6d5d9a61..28d993dacd 100644 --- a/src/ts/interface/popup.ts +++ b/src/ts/interface/popup.ts @@ -30,4 +30,6 @@ export interface PopupSettings extends Popup { export interface PopupUsecase extends Popup { onPage(page: string, data: any): void; + getAuthor(author: string): string; + onAuthor(author: string): void; }; \ No newline at end of file diff --git a/src/ts/lib/api/response.ts b/src/ts/lib/api/response.ts index 66199b6095..b72d40460f 100644 --- a/src/ts/lib/api/response.ts +++ b/src/ts/lib/api/response.ts @@ -427,7 +427,8 @@ export const GalleryDownloadIndex = (response: Rpc.Gallery.DownloadIndex.Respons return { categories: (response.getCategoriesList() || []).map((it: Rpc.Gallery.DownloadIndex.Response.Category) => { return { - name: it.getName(), + id: it.getId(), + icon: it.getIcon(), list: it.getExperiencesList() || [], }; }), diff --git a/src/ts/store/block.ts b/src/ts/store/block.ts index ab23abfda9..4540bd9d10 100644 --- a/src/ts/store/block.ts +++ b/src/ts/store/block.ts @@ -203,7 +203,7 @@ class BlockStore { return []; }; - const blocks = Array.from(map.values()); + const blocks = Array.from(map.values()).filter(it => it); return filter ? blocks.filter(it => filter(it)) : blocks; }; @@ -431,7 +431,7 @@ class BlockStore { }; updateMarkup (rootId: string) { - const blocks = this.getBlocks(rootId, it => it.isText()); + const blocks = this.getBlocks(rootId, it => it && it.isText()); for (const block of blocks) { let marks = block.content.marks || []; diff --git a/src/ts/store/common.ts b/src/ts/store/common.ts index efceeffc9d..b73291d789 100644 --- a/src/ts/store/common.ts +++ b/src/ts/store/common.ts @@ -50,6 +50,10 @@ class CommonStore { public isSidebarFixedValue = null; public showRelativeDatesValue = null; public fullscreenObjectValue = null; + public gallery = { + categories: [], + list: [], + }; public previewObj: I.Preview = { type: null, diff --git a/src/ts/store/popup.ts b/src/ts/store/popup.ts index a372e7454c..5d113186ff 100644 --- a/src/ts/store/popup.ts +++ b/src/ts/store/popup.ts @@ -15,6 +15,7 @@ const SHOW_DIMMER = [ 'about', 'inviteRequest', 'inviteConfirm', + 'usecase', ]; class PopupStore {