mirror of
https://github.com/logseq/logseq.git
synced 2026-05-23 20:24:15 +00:00
Merge branch 'master' into enhance/keymaps-manager-x
This commit is contained in:
44
.github/workflows/build-desktop-release.yml
vendored
44
.github/workflows/build-desktop-release.yml
vendored
@@ -171,6 +171,46 @@ jobs:
|
||||
name: static
|
||||
path: static
|
||||
|
||||
e2e-test:
|
||||
name: E2E Test Shard ${{ matrix.shard }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
shard: [1, 2, 3]
|
||||
needs: [ compile-cljs ]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Download The Static Asset
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: static
|
||||
path: static
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: 'yarn'
|
||||
cache-dependency-path: |
|
||||
yarn.lock
|
||||
static/yarn.lock
|
||||
|
||||
- name: Fetch yarn deps for E2E test
|
||||
run: |
|
||||
yarn install
|
||||
(cd static && yarn install && yarn rebuild:all)
|
||||
env:
|
||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: true
|
||||
|
||||
- name: Run Playwright test
|
||||
run: xvfb-run -- npx playwright test --reporter github --shard=${{ matrix.shard }}/3
|
||||
env:
|
||||
LOGSEQ_CI: true
|
||||
DEBUG: "pw:api"
|
||||
RELEASE: true # skip dev only test
|
||||
|
||||
build-linux:
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [ compile-cljs ]
|
||||
@@ -436,7 +476,7 @@ jobs:
|
||||
|
||||
nightly-release:
|
||||
if: ${{ github.event_name == 'schedule' || github.event.inputs.build-target == 'nightly' }}
|
||||
needs: [ build-macos-x64, build-macos-arm64, build-linux, build-windows, build-android ]
|
||||
needs: [ build-macos-x64, build-macos-arm64, build-linux, build-windows, build-android, e2e-test ]
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Download MacOS x64 Artifacts
|
||||
@@ -503,7 +543,7 @@ jobs:
|
||||
release:
|
||||
# NOTE: For now, we only have beta channel to be released on Github
|
||||
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.build-target == 'beta' }}
|
||||
needs: [ build-macos-x64, build-macos-arm64, build-linux, build-windows ]
|
||||
needs: [ build-macos-x64, build-macos-arm64, build-linux, build-windows, e2e-test ]
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Download MacOS x64 Artifacts
|
||||
|
||||
@@ -6,8 +6,8 @@ android {
|
||||
applicationId "com.logseq.app"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 59
|
||||
versionName "0.9.6"
|
||||
versionCode 61
|
||||
versionName "0.9.8"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
aaptOptions {
|
||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||
|
||||
@@ -92,10 +92,23 @@ public class FsWatcher extends Plugin {
|
||||
shouldRead = true;
|
||||
}
|
||||
|
||||
URI dir = (new File(mPath)).toURI();
|
||||
URI fpath = f.toURI();
|
||||
Uri dir = Uri.fromFile(new File(mPath));
|
||||
Uri fpath = Uri.fromFile(f);
|
||||
String relpath = null;
|
||||
|
||||
obj.put("path", Normalizer.normalize(dir.relativize(fpath).toString(), Normalizer.Form.NFC));
|
||||
if (fpath.getPath().startsWith(dir.getPath())) {
|
||||
relpath = fpath.getPath().substring(dir.getPath().length());
|
||||
if (relpath.startsWith("/")) {
|
||||
relpath = relpath.substring(1);
|
||||
}
|
||||
relpath = Uri.decode(relpath);
|
||||
} else {
|
||||
Log.e("FsWatcher", "file path not under watch path");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
obj.put("path", Normalizer.normalize(relpath, Normalizer.Form.NFC));
|
||||
obj.put("dir", Uri.fromFile(new File(mPath))); // Uri is for Android. URI is for RFC compatible
|
||||
JSObject stat;
|
||||
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import { expect } from '@playwright/test'
|
||||
import { test } from './fixtures'
|
||||
import { createRandomPage, enterNextBlock, modKey } from './utils'
|
||||
import {
|
||||
createRandomPage,
|
||||
enterNextBlock,
|
||||
modKey,
|
||||
repeatKeyPress,
|
||||
moveCursor,
|
||||
selectCharacters,
|
||||
getSelection,
|
||||
getCursorPos,
|
||||
} from './utils'
|
||||
import { dispatch_kb_events } from './util/keyboard-events'
|
||||
import * as kb_events from './util/keyboard-events'
|
||||
|
||||
@@ -619,3 +628,192 @@ test('should keep correct undo and redo seq after indenting or outdenting the bl
|
||||
await page.keyboard.press(modKey + '+Shift+z')
|
||||
await expect(page.locator('textarea >> nth=0')).toHaveText("aaa bbb")
|
||||
})
|
||||
|
||||
test.describe('Text Formatting', () => {
|
||||
const formats = [
|
||||
{ name: 'bold', prefix: '**', postfix: '**', shortcut: modKey + '+b' },
|
||||
{ name: 'italic', prefix: '*', postfix: '*', shortcut: modKey + '+i' },
|
||||
{
|
||||
name: 'strikethrough',
|
||||
prefix: '~~',
|
||||
postfix: '~~',
|
||||
shortcut: modKey + '+Shift+s',
|
||||
},
|
||||
// {
|
||||
// name: 'underline',
|
||||
// prefix: '<u>',
|
||||
// postfix: '</u>',
|
||||
// shortcut: modKey + '+u',
|
||||
// },
|
||||
]
|
||||
|
||||
for (const format of formats) {
|
||||
test.describe(`${format.name} formatting`, () => {
|
||||
test('Applying to an empty selection inserts placeholder formatting and places cursor correctly', async ({
|
||||
page,
|
||||
block,
|
||||
}) => {
|
||||
await createRandomPage(page)
|
||||
|
||||
const text = 'Lorem ipsum'
|
||||
await block.mustFill(text)
|
||||
|
||||
// move the cursor to the end of Lorem
|
||||
await repeatKeyPress(page, 'ArrowLeft', text.length - 'ipsum'.length)
|
||||
await page.keyboard.press('Space')
|
||||
|
||||
// Apply formatting
|
||||
await page.keyboard.press(format.shortcut)
|
||||
|
||||
await expect(page.locator('textarea >> nth=0')).toHaveText(
|
||||
`Lorem ${format.prefix}${format.postfix} ipsum`
|
||||
)
|
||||
|
||||
// Verify cursor position
|
||||
const cursorPos = await getCursorPos(page)
|
||||
expect(cursorPos).toBe(' ipsum'.length + format.prefix.length)
|
||||
})
|
||||
|
||||
test('Applying to an entire block encloses the block in formatting and places cursor correctly', async ({
|
||||
page,
|
||||
block,
|
||||
}) => {
|
||||
await createRandomPage(page)
|
||||
|
||||
const text = 'Lorem ipsum-dolor sit.'
|
||||
await block.mustFill(text)
|
||||
|
||||
// Select the entire block
|
||||
await page.keyboard.press(modKey + '+a')
|
||||
|
||||
// Apply formatting
|
||||
await page.keyboard.press(format.shortcut)
|
||||
|
||||
await expect(page.locator('textarea >> nth=0')).toHaveText(
|
||||
`${format.prefix}${text}${format.postfix}`
|
||||
)
|
||||
|
||||
// Verify cursor position
|
||||
const cursorPosition = await getCursorPos(page)
|
||||
expect(cursorPosition).toBe(format.prefix.length + text.length)
|
||||
})
|
||||
|
||||
test('Applying and then removing from a word connected with a special character correctly formats and then reverts', async ({
|
||||
page,
|
||||
block,
|
||||
}) => {
|
||||
await createRandomPage(page)
|
||||
|
||||
await block.mustFill('Lorem ipsum-dolor sit.')
|
||||
|
||||
// Select 'ipsum'
|
||||
// Move the cursor to the desired position
|
||||
await moveCursor(page, -16)
|
||||
|
||||
// Select the desired length of text
|
||||
await selectCharacters(page, 5)
|
||||
|
||||
// Apply formatting
|
||||
await page.keyboard.press(format.shortcut)
|
||||
|
||||
// Verify that 'ipsum' is formatted
|
||||
await expect(page.locator('textarea >> nth=0')).toHaveText(
|
||||
`Lorem ${format.prefix}ipsum${format.postfix}-dolor sit.`
|
||||
)
|
||||
|
||||
// Re-select 'ipsum'
|
||||
// Move the cursor to the desired position
|
||||
await moveCursor(page, -5)
|
||||
|
||||
// Select the desired length of text
|
||||
await selectCharacters(page, 5)
|
||||
|
||||
// Remove formatting
|
||||
await page.keyboard.press(format.shortcut)
|
||||
await expect(page.locator('textarea >> nth=0')).toHaveText(
|
||||
'Lorem ipsum-dolor sit.'
|
||||
)
|
||||
|
||||
// Verify the word 'ipsum' is still selected
|
||||
const selection = await getSelection(page)
|
||||
expect(selection).toBe('ipsum')
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
test.describe('Always auto-pair symbols', () => {
|
||||
// Define the symbols that should be auto-paired
|
||||
const autoPairSymbols = [
|
||||
{ name: 'square brackets', prefix: '[', postfix: ']' },
|
||||
{ name: 'curly brackets', prefix: '{', postfix: '}' },
|
||||
{ name: 'parentheses', prefix: '(', postfix: ')' },
|
||||
// { name: 'angle brackets', prefix: '<', postfix: '>' },
|
||||
{ name: 'backtick', prefix: '`', postfix: '`' },
|
||||
// { name: 'single quote', prefix: "'", postfix: "'" },
|
||||
// { name: 'double quote', prefix: '"', postfix: '"' },
|
||||
]
|
||||
|
||||
for (const symbol of autoPairSymbols) {
|
||||
test(`${symbol.name} auto-pairing`, async ({ page }) => {
|
||||
await createRandomPage(page)
|
||||
|
||||
// Type prefix and check that the postfix is automatically added
|
||||
page.type('textarea >> nth=0', symbol.prefix, { delay: 100 })
|
||||
await expect(page.locator('textarea >> nth=0')).toHaveText(
|
||||
`${symbol.prefix}${symbol.postfix}`
|
||||
)
|
||||
|
||||
// Check that the cursor is positioned correctly between the prefix and postfix
|
||||
const CursorPos = await getCursorPos(page)
|
||||
expect(CursorPos).toBe(symbol.prefix.length)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
test.describe('Auto-pair symbols only with text selection', () => {
|
||||
const autoPairSymbols = [
|
||||
// { name: 'tilde', prefix: '~', postfix: '~' },
|
||||
{ name: 'asterisk', prefix: '*', postfix: '*' },
|
||||
{ name: 'underscore', prefix: '_', postfix: '_' },
|
||||
{ name: 'caret', prefix: '^', postfix: '^' },
|
||||
{ name: 'equal', prefix: '=', postfix: '=' },
|
||||
{ name: 'slash', prefix: '/', postfix: '/' },
|
||||
{ name: 'plus', prefix: '+', postfix: '+' },
|
||||
]
|
||||
|
||||
for (const symbol of autoPairSymbols) {
|
||||
test(`Only auto-pair ${symbol.name} with text selection`, async ({
|
||||
page,
|
||||
block,
|
||||
}) => {
|
||||
await createRandomPage(page)
|
||||
|
||||
// type the symbol
|
||||
page.type('textarea >> nth=0', symbol.prefix, { delay: 100 })
|
||||
|
||||
// Verify that there is no auto-pairing
|
||||
await expect(page.locator('textarea >> nth=0')).toHaveText(symbol.prefix)
|
||||
|
||||
// remove prefix
|
||||
await page.keyboard.press('Backspace')
|
||||
|
||||
// add text
|
||||
await block.mustType('Lorem')
|
||||
// select text
|
||||
await page.keyboard.press(modKey + '+a')
|
||||
|
||||
// Type the prefix
|
||||
await page.type('textarea >> nth=0', symbol.prefix, { delay: 100 })
|
||||
|
||||
// Verify that an additional postfix was automatically added around 'Lorem'
|
||||
await expect(page.locator('textarea >> nth=0')).toHaveText(
|
||||
`${symbol.prefix}Lorem${symbol.postfix}`
|
||||
)
|
||||
|
||||
// Verify 'Lorem' is selected
|
||||
const selection = await getSelection(page)
|
||||
expect(selection).toBe('Lorem')
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -237,3 +237,68 @@ export async function navigateToStartOfBlock(page: Page, block: Block) {
|
||||
await page.keyboard.press('ArrowLeft')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Repeats a key press a certain number of times.
|
||||
* @param {Page} page - The Page object.
|
||||
* @param {string} key - The key to press.
|
||||
* @param {number} times - The number of times to press the key.
|
||||
* @return {Promise<void>} - Promise which resolves when the key press repetition is done.
|
||||
*/
|
||||
export async function repeatKeyPress(page: Page, key: string, times: number): Promise<void> {
|
||||
for (let i = 0; i < times; i++) {
|
||||
await page.keyboard.press(key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the cursor a certain number of characters to the right (positive value) or left (negative value).
|
||||
* @param {Page} page - The Page object.
|
||||
* @param {number} shift - The number of characters to move the cursor. Positive moves to the right, negative to the left.
|
||||
* @return {Promise<void>} - Promise which resolves when the cursor has moved.
|
||||
*/
|
||||
export async function moveCursor(page: Page, shift: number): Promise<void> {
|
||||
const direction = shift < 0 ? 'ArrowLeft' : 'ArrowRight';
|
||||
const absShift = Math.abs(shift);
|
||||
await repeatKeyPress(page, direction, absShift);
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects a certain length of text in a textarea to the right of the cursor.
|
||||
* @param {Page} page - The Page object.
|
||||
* @param {number} length - The number of characters to select.
|
||||
* @return {Promise<void>} - Promise which resolves when the text selection is done.
|
||||
*/
|
||||
export async function selectCharacters(page: Page, length: number): Promise<void> {
|
||||
await page.keyboard.down('Shift');
|
||||
await repeatKeyPress(page, 'ArrowRight', length);
|
||||
await page.keyboard.up('Shift');
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the selected text in a textarea.
|
||||
* @param {Page} page - The page object.
|
||||
* @return {Promise<string | null>} - Promise which resolves to the selected text or null.
|
||||
*/
|
||||
export async function getSelection(page: Page): Promise<string | null> {
|
||||
const selection = await page.evaluate(() => {
|
||||
const textarea = document.querySelector('textarea')
|
||||
return textarea?.value.substring(textarea.selectionStart, textarea.selectionEnd) || null
|
||||
})
|
||||
|
||||
return selection
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the current cursor position in a textarea.
|
||||
* @param {Page} page - The page object.
|
||||
* @return {Promise<number | null>} - Promise which resolves to the cursor position or null.
|
||||
*/
|
||||
export async function getCursorPos(page: Page): Promise<number | null> {
|
||||
const cursorPosition = await page.evaluate(() => {
|
||||
const textarea = document.querySelector('textarea');
|
||||
return textarea ? textarea.selectionStart : null;
|
||||
});
|
||||
|
||||
return cursorPosition;
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
D3D62A0C275C928F0003FBDC /* FileContainer.m in Sources */ = {isa = PBXBuildFile; fileRef = D3D62A0B275C928F0003FBDC /* FileContainer.m */; };
|
||||
FE647FF427BDFEDE00F3206B /* FsWatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE647FF327BDFEDE00F3206B /* FsWatcher.swift */; };
|
||||
FE647FF627BDFEF500F3206B /* FsWatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = FE647FF527BDFEF500F3206B /* FsWatcher.m */; };
|
||||
FE96D6102A1B811A001ECE32 /* SharedData.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE96D60F2A1B811A001ECE32 /* SharedData.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -83,6 +84,7 @@
|
||||
DE5650F4AD4E2242AB9C012D /* Pods-Logseq.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Logseq.debug.xcconfig"; path = "Target Support Files/Pods-Logseq/Pods-Logseq.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
FE647FF327BDFEDE00F3206B /* FsWatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FsWatcher.swift; sourceTree = "<group>"; };
|
||||
FE647FF527BDFEF500F3206B /* FsWatcher.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FsWatcher.m; sourceTree = "<group>"; };
|
||||
FE96D60F2A1B811A001ECE32 /* SharedData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedData.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -156,6 +158,7 @@
|
||||
children = (
|
||||
5FFF7D7927E4E70700B00DA8 /* ShareViewController.entitlements */,
|
||||
5FFF7D6C27E343FA00B00DA8 /* ShareViewController.swift */,
|
||||
FE96D60F2A1B811A001ECE32 /* SharedData.swift */,
|
||||
5FFF7D6E27E343FA00B00DA8 /* MainInterface.storyboard */,
|
||||
5FFF7D7127E343FA00B00DA8 /* Info.plist */,
|
||||
);
|
||||
@@ -345,6 +348,7 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
FE96D6102A1B811A001ECE32 /* SharedData.swift in Sources */,
|
||||
5FFF7D6D27E343FA00B00DA8 /* ShareViewController.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -438,7 +442,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
@@ -492,7 +496,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
|
||||
@@ -515,7 +519,7 @@
|
||||
INFOPLIST_FILE = App/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MARKETING_VERSION = 0.9.6;
|
||||
MARKETING_VERSION = 0.9.8;
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -542,7 +546,7 @@
|
||||
INFOPLIST_FILE = App/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MARKETING_VERSION = 0.9.6;
|
||||
MARKETING_VERSION = 0.9.8;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
|
||||
@@ -565,9 +569,9 @@
|
||||
INFOPLIST_FILE = ShareViewController/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = ShareViewController;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
|
||||
MARKETING_VERSION = 0.9.6;
|
||||
MARKETING_VERSION = 0.9.8;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq.ShareViewController;
|
||||
@@ -592,9 +596,9 @@
|
||||
INFOPLIST_FILE = ShareViewController/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = ShareViewController;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
|
||||
MARKETING_VERSION = 0.9.6;
|
||||
MARKETING_VERSION = 0.9.8;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq.ShareViewController;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
<dict>
|
||||
<key>NSExtensionActivationRule</key>
|
||||
<dict>
|
||||
<key>NSExtensionActivationDictionaryVersion</key>
|
||||
<integer>2</integer>
|
||||
<key>NSExtensionActivationSupportsFileWithMaxCount</key>
|
||||
<integer>5</integer>
|
||||
<key>NSExtensionActivationSupportsImageWithMaxCount</key>
|
||||
@@ -17,9 +19,9 @@
|
||||
<key>NSExtensionActivationSupportsText</key>
|
||||
<true/>
|
||||
<key>NSExtensionActivationSupportsWebPageWithMaxCount</key>
|
||||
<integer>1</integer>
|
||||
<integer>3</integer>
|
||||
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
|
||||
<integer>1</integer>
|
||||
<integer>3</integer>
|
||||
<key>NSExtensionActivationUsesStrictMatching</key>
|
||||
<false/>
|
||||
</dict>
|
||||
|
||||
@@ -9,182 +9,180 @@
|
||||
import MobileCoreServices
|
||||
import Social
|
||||
import UIKit
|
||||
|
||||
class ShareItem {
|
||||
public var title: String?
|
||||
public var type: String?
|
||||
public var url: String?
|
||||
}
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
class ShareViewController: UIViewController {
|
||||
|
||||
private var shareItems: [ShareItem] = []
|
||||
|
||||
|
||||
private var sharedData: SharedData = SharedData.init(resources: [])
|
||||
|
||||
var groupContainerUrl: URL? {
|
||||
return FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.com.logseq.logseq")
|
||||
}
|
||||
|
||||
|
||||
override public func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
|
||||
super.viewDidAppear(animated)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
|
||||
self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func sendData() {
|
||||
let queryItems = shareItems.map {
|
||||
let encoder: JSONEncoder = JSONEncoder()
|
||||
let data = try? encoder.encode(self.sharedData)
|
||||
let queryPayload = String(decoding: data!, as: UTF8.self)
|
||||
|
||||
let queryItems =
|
||||
[
|
||||
URLQueryItem(
|
||||
name: "title",
|
||||
value: $0.title?.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? ""),
|
||||
URLQueryItem(name: "description", value: ""),
|
||||
URLQueryItem(
|
||||
name: "type",
|
||||
value: $0.type?.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? ""),
|
||||
URLQueryItem(
|
||||
name: "url",
|
||||
value: $0.url?.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? ""),
|
||||
name: "payload",
|
||||
value: queryPayload.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? ""),
|
||||
]
|
||||
}.flatMap({ $0 })
|
||||
var urlComps = URLComponents(string: "logseq://shared?")!
|
||||
urlComps.queryItems = queryItems
|
||||
openURL(urlComps.url!)
|
||||
}
|
||||
|
||||
fileprivate func createSharedFileUrl(_ url: URL?) -> String {
|
||||
|
||||
let copyFileUrl = groupContainerUrl!.absoluteString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)! + "/" + url!
|
||||
|
||||
fileprivate func createSharedFileUrl(_ url: URL?) -> URL? {
|
||||
let tempFilename = url!
|
||||
.lastPathComponent.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
|
||||
try? Data(contentsOf: url!).write(to: URL(string: copyFileUrl)!)
|
||||
|
||||
let copyFileUrl = groupContainerUrl!.appendingPathComponent(tempFilename)
|
||||
try? Data(contentsOf: url!).write(to: copyFileUrl)
|
||||
return copyFileUrl
|
||||
}
|
||||
|
||||
func saveScreenshot(_ image: UIImage) -> String {
|
||||
|
||||
|
||||
// Screenshots, shared images from some system App are passed as UIImage
|
||||
func saveUIImage(_ image: UIImage) -> URL? {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateFormat = "yyyy-MM-dd-HH-mm-ss"
|
||||
|
||||
let copyFileUrl = groupContainerUrl!.absoluteString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
|
||||
+ dateFormatter.string(from: Date()) + ".png"
|
||||
|
||||
let filename = dateFormatter.string(from: Date()) + ".png"
|
||||
|
||||
let copyFileUrl = groupContainerUrl!.appendingPathComponent(filename)
|
||||
|
||||
do {
|
||||
try image.pngData()?.write(to: URL(string: copyFileUrl)!)
|
||||
try image.pngData()?.write(to: copyFileUrl)
|
||||
return copyFileUrl
|
||||
} catch {
|
||||
print(error.localizedDescription)
|
||||
return ""
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Can be a path or a web URL
|
||||
fileprivate func handleTypeUrl(_ attachment: NSItemProvider)
|
||||
async throws -> ShareItem
|
||||
async throws -> SharedResource
|
||||
{
|
||||
let results = try await attachment.loadItem(forTypeIdentifier: kUTTypeURL as String, options: nil)
|
||||
let url = results as! URL?
|
||||
let shareItem: ShareItem = ShareItem()
|
||||
|
||||
|
||||
var res = SharedResource()
|
||||
|
||||
if url!.isFileURL {
|
||||
shareItem.title = url!.lastPathComponent
|
||||
shareItem.type = "application/" + url!.pathExtension.lowercased()
|
||||
shareItem.url = createSharedFileUrl(url)
|
||||
res.name = url!.lastPathComponent
|
||||
res.ext = url!.pathExtension
|
||||
res.type = url!.pathExtensionAsMimeType()
|
||||
res.url = createSharedFileUrl(url)
|
||||
} else {
|
||||
shareItem.title = url!.absoluteString
|
||||
shareItem.url = url!.absoluteString
|
||||
shareItem.type = "text/plain"
|
||||
res.name = url!.absoluteString
|
||||
res.type = "text/plain"
|
||||
}
|
||||
|
||||
return shareItem
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
|
||||
fileprivate func handleTypeText(_ attachment: NSItemProvider)
|
||||
async throws -> ShareItem
|
||||
async throws -> SharedResource?
|
||||
{
|
||||
let results = try await attachment.loadItem(forTypeIdentifier: kUTTypeText as String, options: nil)
|
||||
let shareItem: ShareItem = ShareItem()
|
||||
let text = results as! String
|
||||
shareItem.title = text
|
||||
shareItem.type = "text/plain"
|
||||
|
||||
return shareItem
|
||||
let item = try await attachment.loadItem(forTypeIdentifier: kUTTypeText as String, options: nil)
|
||||
self.sharedData.text = item as? String
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
fileprivate func handleTypeMovie(_ attachment: NSItemProvider)
|
||||
async throws -> ShareItem
|
||||
async throws -> SharedResource
|
||||
{
|
||||
let results = try await attachment.loadItem(forTypeIdentifier: kUTTypeMovie as String, options: nil)
|
||||
let shareItem: ShareItem = ShareItem()
|
||||
|
||||
|
||||
let url = results as! URL?
|
||||
shareItem.title = url!.lastPathComponent
|
||||
shareItem.type = "video/" + url!.pathExtension.lowercased()
|
||||
shareItem.url = createSharedFileUrl(url)
|
||||
|
||||
return shareItem
|
||||
|
||||
let name = url!.lastPathComponent
|
||||
let ext = url!.pathExtension.lowercased()
|
||||
let type = url!.pathExtensionAsMimeType()
|
||||
let sharedUrl = createSharedFileUrl(url)
|
||||
|
||||
let res = SharedResource(name: name, ext: ext, type: type, url: sharedUrl)
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
|
||||
fileprivate func handleTypeImage(_ attachment: NSItemProvider)
|
||||
async throws -> ShareItem
|
||||
async throws -> SharedResource
|
||||
{
|
||||
let data = try await attachment.loadItem(forTypeIdentifier: kUTTypeImage as String, options: nil)
|
||||
|
||||
let shareItem: ShareItem = ShareItem()
|
||||
|
||||
var res = SharedResource()
|
||||
|
||||
switch data {
|
||||
case let image as UIImage:
|
||||
shareItem.title = "screenshot"
|
||||
shareItem.type = "image/png"
|
||||
shareItem.url = self.saveScreenshot(image)
|
||||
res.url = self.saveUIImage(image)
|
||||
res.ext = "png"
|
||||
res.name = res.url?.lastPathComponent
|
||||
res.type = res.url?.pathExtensionAsMimeType()
|
||||
case let url as URL:
|
||||
shareItem.title = url.lastPathComponent
|
||||
shareItem.type = "image/" + url.pathExtension.lowercased()
|
||||
shareItem.url = self.createSharedFileUrl(url)
|
||||
res.name = url.lastPathComponent
|
||||
res.ext = url.pathExtension.lowercased()
|
||||
res.type = url.pathExtensionAsMimeType()
|
||||
res.url = self.createSharedFileUrl(url)
|
||||
default:
|
||||
print("Unexpected image data:", type(of: data))
|
||||
}
|
||||
|
||||
return shareItem
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
override public func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
shareItems.removeAll()
|
||||
|
||||
let extensionItem = extensionContext?.inputItems.first as! NSExtensionItem
|
||||
|
||||
sharedData.empty()
|
||||
let inputItems = extensionContext?.inputItems as! [NSExtensionItem]
|
||||
Task {
|
||||
try await withThrowingTaskGroup(
|
||||
of: ShareItem.self,
|
||||
of: SharedResource?.self,
|
||||
body: { taskGroup in
|
||||
|
||||
for attachment in extensionItem.attachments! {
|
||||
if attachment.hasItemConformingToTypeIdentifier(kUTTypeURL as String) {
|
||||
taskGroup.addTask {
|
||||
return try await self.handleTypeUrl(attachment)
|
||||
}
|
||||
} else if attachment.hasItemConformingToTypeIdentifier(kUTTypeText as String) {
|
||||
taskGroup.addTask {
|
||||
return try await self.handleTypeText(attachment)
|
||||
}
|
||||
} else if attachment.hasItemConformingToTypeIdentifier(kUTTypeMovie as String) {
|
||||
taskGroup.addTask {
|
||||
return try await self.handleTypeMovie(attachment)
|
||||
}
|
||||
} else if attachment.hasItemConformingToTypeIdentifier(kUTTypeImage as String) {
|
||||
taskGroup.addTask {
|
||||
return try await self.handleTypeImage(attachment)
|
||||
for extensionItem in inputItems {
|
||||
for attachment in extensionItem.attachments! {
|
||||
if attachment.hasItemConformingToTypeIdentifier(kUTTypeURL as String) {
|
||||
taskGroup.addTask {
|
||||
return try await self.handleTypeUrl(attachment)
|
||||
}
|
||||
} else if attachment.hasItemConformingToTypeIdentifier(kUTTypeText as String) {
|
||||
taskGroup.addTask {
|
||||
return try await self.handleTypeText(attachment)
|
||||
}
|
||||
} else if attachment.hasItemConformingToTypeIdentifier(kUTTypeMovie as String) {
|
||||
taskGroup.addTask {
|
||||
return try await self.handleTypeMovie(attachment)
|
||||
}
|
||||
} else if attachment.hasItemConformingToTypeIdentifier(kUTTypeImage as String) {
|
||||
taskGroup.addTask {
|
||||
return try await self.handleTypeImage(attachment)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
for try await item in taskGroup {
|
||||
self.shareItems.append(item)
|
||||
if let item = item {
|
||||
self.sharedData.resources.append(item)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
self.sendData()
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@discardableResult
|
||||
@objc func openURL(_ url: URL) -> Bool {
|
||||
var responder: UIResponder? = self
|
||||
@@ -196,6 +194,14 @@ class ShareViewController: UIViewController {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
extension URL {
|
||||
func pathExtensionAsMimeType() -> String? {
|
||||
let type = UTType(filenameExtension: self.pathExtension)
|
||||
return type?.preferredMIMEType
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
25
ios/App/ShareViewController/SharedData.swift
Normal file
25
ios/App/ShareViewController/SharedData.swift
Normal file
@@ -0,0 +1,25 @@
|
||||
//
|
||||
// SharedData.swift
|
||||
// ShareViewController
|
||||
//
|
||||
// Created by Mono Wang on 5/22/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct SharedResource: Decodable, Encodable {
|
||||
var name: String?
|
||||
var ext: String?
|
||||
var type: String?
|
||||
var url: URL?
|
||||
}
|
||||
|
||||
public struct SharedData: Decodable, Encodable {
|
||||
var text: String?
|
||||
var resources: [SharedResource]
|
||||
|
||||
mutating func empty() {
|
||||
text = nil
|
||||
resources = []
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "Logseq",
|
||||
"productName": "Logseq",
|
||||
"version": "0.9.6",
|
||||
"version": "0.9.8",
|
||||
"main": "electron.js",
|
||||
"author": "Logseq",
|
||||
"license": "AGPL-3.0",
|
||||
|
||||
@@ -65,6 +65,7 @@
|
||||
:right-side-bar/switch-theme "Тема"
|
||||
:right-side-bar/contents "Содержание"
|
||||
:right-side-bar/page-graph "Граф страницы"
|
||||
:right-side-bar/history "(Dev) Отменить/Повторить историю"
|
||||
:right-side-bar/block-ref "Ссылка на блок"
|
||||
:right-side-bar/graph-view "Визуальный граф"
|
||||
:right-side-bar/all-pages "Все страницы"
|
||||
@@ -151,10 +152,31 @@
|
||||
:color/pink "Розовый"
|
||||
:editor/copy "Копировать"
|
||||
:editor/cut "Вырезать"
|
||||
:editor/expand-block-children "Раскрыть всё"
|
||||
:editor/collapse-block-children "Свернуть всё"
|
||||
:editor/delete-selection "Удалить выбранные блоки"
|
||||
:editor/cycle-todo "Изменить статус TODO текущего элемента"
|
||||
:dev/show-page-data "(Dev) Показать данные страницы"
|
||||
:dev/show-block-data "(Dev) Показать данные блока"
|
||||
:dev/show-block-ast "(Dev) Показать AST блока"
|
||||
:dev/show-page-ast "(Dev) Показать AST страницы"
|
||||
:content/copy-export-as "Копировать / Экспортировать как.."
|
||||
:content/copy-block-url "Копировать URL блока"
|
||||
:content/copy-block-ref "Копировать ссылку блока"
|
||||
:content/copy-block-emebed "Копировать встроенный блок"
|
||||
:content/copy-ref "Скопировать эту ссылку"
|
||||
:content/delete-ref "Удалить эту ссылку"
|
||||
:content/replace-with-text "Заменить на текст"
|
||||
:content/replace-with-embed "Заменить на встроенный элемент"
|
||||
:content/open-in-sidebar "Открыть в боковой панели"
|
||||
:content/click-to-edit "Нажмите для редактирования"
|
||||
:context-menu/make-a-flashcard "Создать карточку"
|
||||
:context-menu/toggle-number-list "Переключить номерной список"
|
||||
:context-menu/preview-flashcard "Предварительный просмотр карточки"
|
||||
:context-menu/make-a-template "Создать шаблон"
|
||||
:context-menu/input-template-name "Как назовём шаблон?"
|
||||
:context-menu/template-include-parent-block "Включить родительский блок в шаблон?"
|
||||
:context-menu/template-exists-warning "Шаблон уже существует!"
|
||||
:settings-page/git-confirm "Необходимо перезапустить приложение после изменения настроек Git."
|
||||
:settings-page/git-switcher-label "Включить автокоммит в Git"
|
||||
:settings-page/git-commit-delay "Задержка автокоммита Git в секундах"
|
||||
@@ -294,6 +316,10 @@
|
||||
:plugin/update "Обновить"
|
||||
:plugin/check-update "Проверить обновления"
|
||||
:plugin/check-all-updates "Проверить все обновления"
|
||||
:plugin/found-updates "Новые обновления"
|
||||
:plugin/found-n-updates "Найдено обновлений {1}"
|
||||
:plugin/update-all-selected "Обновить все выбранные"
|
||||
:plugin/updates-downloading "Загрузка обновлений"
|
||||
:plugin/refresh-lists "Обновить списки"
|
||||
:plugin/enabled "Включено"
|
||||
:plugin/disabled "Отключено"
|
||||
@@ -345,7 +371,8 @@
|
||||
|
||||
:file-sync/other-user-graph "Текущий локальный граф привязан к удаленному графу другого пользователя. Поэтому синхронизацию начать нельзя."
|
||||
:file-sync/graph-deleted "Текущий удаленный граф был удален"
|
||||
|
||||
:file-sync/rsapi-cannot-upload-err "Невозможно начать синхронизацию, пожалуйста, проверьте правильное ли установлено локальное время."
|
||||
|
||||
:notification/clear-all "Очистить всё"
|
||||
|
||||
:shortcut.category/basics "Базовые"
|
||||
@@ -355,16 +382,17 @@
|
||||
:shortcut.category/block-command-editing "Команды редактирования блока"
|
||||
:shortcut.category/block-selection "Выделение блоков (нажмите Esc для отмены)"
|
||||
:shortcut.category/toggle "Переключатели"
|
||||
:shortcut.category/whiteboard "Интерактивная доска"
|
||||
:shortcut.category/others "Разное"
|
||||
:command.date-picker/complete "Выбор даты: Выбрать указанный день"
|
||||
:command.date-picker/prev-day "Выбор даты: Выбрать предыдущий день"
|
||||
:command.date-picker/next-day "Выбор даты: Выбрать следующий день"
|
||||
:command.date-picker/prev-week "Выбор даты: Выбрать предыдущую неделю"
|
||||
:command.date-picker/next-week "Выбор даты: Выбрать следующую неделю"
|
||||
:command.pdf/previous-page "Предыдущая страница текущего PDF"
|
||||
:command.pdf/next-page "Следующая страница текущего PDF"
|
||||
:command.pdf/close "Закрыть текущий просмотр PDF"
|
||||
:command.pdf/find "Pdf: Поиск текста в текущем PDF-документе"
|
||||
:command.pdf/previous-page "PDF: Предыдущая страница текущего PDF"
|
||||
:command.pdf/next-page "PDF: Следующая страница текущего PDF"
|
||||
:command.pdf/close "PDF: Закрыть текущий просмотр PDF"
|
||||
:command.pdf/find "PDF: Поиск текста в текущем PDF-документе"
|
||||
:command.auto-complete/complete "Автодополнение: Использовать выбранный элемент"
|
||||
:command.auto-complete/prev "Автодополнение: Выбрать предыдущий"
|
||||
:command.auto-complete/next "Автодополнение: Выбрать следующий"
|
||||
@@ -400,7 +428,7 @@
|
||||
:command.editor/replace-block-reference-at-point "Заменить ссылку на блок своим содержимым в указанном месте"
|
||||
:command.editor/paste-text-in-one-block-at-point "Вставить текст в один блок в указанном месте"
|
||||
:command.editor/insert-youtube-timestamp "Вставить временную метку на Youtube"
|
||||
:command.editor/cycle-todo "Переключить статус данной задачи (TODO)"
|
||||
:command.editor/cycle-todo "Изменить статус TODO текущего элемента"
|
||||
:command.editor/up "Переместить курсор вверх / Выбрать вверх"
|
||||
:command.editor/down "Переместить курсор вниз / Выбрать вниз"
|
||||
:command.editor/left "Переместить курсор влево / Открыть выбранный блок в начале"
|
||||
@@ -427,6 +455,32 @@
|
||||
:command.editor/select-parent "Выбрать родительский блок"
|
||||
:command.editor/zoom-in "Увеличить / Вперед"
|
||||
:command.editor/zoom-out "Уменьшить / Назад"
|
||||
:command.editor/toggle-undo-redo-mode "Переключить режим отменить/повторить (глобально или только на странице)"
|
||||
:command.editor/toggle-number-list "Переключить режим нумерованный список"
|
||||
:command.whiteboard/select "Выбрать инструмент"
|
||||
:command.whiteboard/pan "Прокруктка"
|
||||
:command.whiteboard/portal "Добавить блок или страницу"
|
||||
:command.whiteboard/pencil "Карандаш"
|
||||
:command.whiteboard/highlighter "Маркер"
|
||||
:command.whiteboard/eraser "Ластик"
|
||||
:command.whiteboard/connector "Соединитель"
|
||||
:command.whiteboard/text "Текст"
|
||||
:command.whiteboard/rectangle "Прямоугольник"
|
||||
:command.whiteboard/ellipse "Эллипс"
|
||||
:command.whiteboard/reset-zoom "Сбросить масштаб"
|
||||
:command.whiteboard/zoom-to-fit "Показать все элементы"
|
||||
:command.whiteboard/zoom-to-selection "Показать элемент"
|
||||
:command.whiteboard/zoom-out "Уменьшить"
|
||||
:command.whiteboard/zoom-in "Увеличить"
|
||||
:command.whiteboard/send-backward "Переместить назад"
|
||||
:command.whiteboard/send-to-back "На задний план"
|
||||
:command.whiteboard/bring-forward "Переместить вперёд"
|
||||
:command.whiteboard/bring-to-front "На передний план"
|
||||
:command.whiteboard/lock "Блокировать"
|
||||
:command.whiteboard/unlock "Разблокировать"
|
||||
:command.whiteboard/group "Группировать"
|
||||
:command.whiteboard/ungroup "Разгруппировать"
|
||||
:command.whiteboard/toggle-grid "Переключить отображение сетки"
|
||||
:command.ui/toggle-brackets "Переключить отображение скобок"
|
||||
:command.go/search-in-page "Поиск блоков на текущей странице"
|
||||
:command.go/electron-find-in-page "Поиск текста на странице"
|
||||
@@ -466,7 +520,7 @@
|
||||
:command.ui/toggle-help "Переключить помощь"
|
||||
:command.ui/toggle-theme "Переключение между темной/светлой темой"
|
||||
:command.ui/toggle-contents "Переключить Контент на боковой панели"
|
||||
;; :ui/open-new-window "Открыть другое окно"
|
||||
;; :command.ui/open-new-window "Открыть новое окно"
|
||||
:command.command/toggle-favorite "Добавить или удалить из избранного"
|
||||
:command.editor/open-file-in-default-app "Открыть файл в программе по умолчанию"
|
||||
:command.editor/open-file-in-directory "Открыть файл в родительском каталоге"
|
||||
|
||||
@@ -1777,7 +1777,7 @@
|
||||
(when (sync-state--valid-to-accept-filewatcher-event? sync-state)
|
||||
(when (or (:mtime stat) (= type "unlink"))
|
||||
(go
|
||||
(let [path (path-normalize (remove-dir-prefix dir path))
|
||||
(let [path (path-normalize path)
|
||||
files-meta (and (not= "unlink" type)
|
||||
(<! (<get-local-files-meta
|
||||
rsapi (:current-syncing-graph-uuid sync-state) dir [path])))
|
||||
|
||||
@@ -1193,10 +1193,6 @@
|
||||
(common-handler/copy-to-clipboard-without-id-property! (:block/format block) md-content html sorted-blocks)
|
||||
(delete-block-aux! block true))))
|
||||
|
||||
(defn clear-last-selected-block!
|
||||
[]
|
||||
(state/drop-last-selection-block!))
|
||||
|
||||
(defn highlight-selection-area!
|
||||
[end-block]
|
||||
(when-let [start-block (state/get-selection-start-block-or-first)]
|
||||
@@ -1212,13 +1208,17 @@
|
||||
(cond
|
||||
;; when editing, quit editing and select current block
|
||||
(state/editing?)
|
||||
(state/exit-editing-and-set-selected-blocks! [(gdom/getElement (state/get-editing-block-dom-id))])
|
||||
(let [element (gdom/getElement (state/get-editing-block-dom-id))]
|
||||
(when element
|
||||
(util/scroll-to-block element)
|
||||
(state/exit-editing-and-set-selected-blocks! [element])))
|
||||
|
||||
;; when selection and one block selected, select next block
|
||||
(and (state/selection?) (== 1 (count (state/get-selection-blocks))))
|
||||
(let [f (if (= :up direction) util/get-prev-block-non-collapsed util/get-next-block-non-collapsed-skip)
|
||||
element (f (first (state/get-selection-blocks)))]
|
||||
(when element
|
||||
(util/scroll-to-block element)
|
||||
(state/conj-selection-block! element direction)))
|
||||
|
||||
;; if same direction, keep conj on same direction
|
||||
@@ -1227,11 +1227,17 @@
|
||||
first-last (if (= :up direction) first last)
|
||||
element (f (first-last (state/get-selection-blocks)))]
|
||||
(when element
|
||||
(util/scroll-to-block element)
|
||||
(state/conj-selection-block! element direction)))
|
||||
|
||||
;; if different direction, keep clear until one left
|
||||
(state/selection?)
|
||||
(clear-last-selected-block!))
|
||||
(let [f (if (= :up direction) util/get-prev-block-non-collapsed util/get-next-block-non-collapsed)
|
||||
last-first (if (= :up direction) last first)
|
||||
element (f (last-first (state/get-selection-blocks)))]
|
||||
(when element
|
||||
(util/scroll-to-block element)
|
||||
(state/drop-last-selection-block!))))
|
||||
nil)
|
||||
|
||||
(defn on-select-block
|
||||
@@ -2482,13 +2488,6 @@
|
||||
(.preventDefault e)
|
||||
(keydown-new-line))))
|
||||
|
||||
(defn- scroll-to-block
|
||||
[block]
|
||||
(when block
|
||||
(when-not (util/element-visible? block)
|
||||
(.scrollIntoView block #js {:behavior "smooth"
|
||||
:block "center"}))))
|
||||
|
||||
(defn- select-first-last
|
||||
"Select first or last block in viewpoint"
|
||||
[direction]
|
||||
@@ -2496,7 +2495,7 @@
|
||||
block (->> (util/get-blocks-noncollapse)
|
||||
(f))]
|
||||
(when block
|
||||
(scroll-to-block block)
|
||||
(util/scroll-to-block block)
|
||||
(state/exit-editing-and-set-selected-blocks! [block]))))
|
||||
|
||||
(defn- select-up-down [direction]
|
||||
@@ -2509,7 +2508,7 @@
|
||||
:down util/get-next-block-non-collapsed)
|
||||
sibling-block (f selected)]
|
||||
(when (and sibling-block (dom/attr sibling-block "blockid"))
|
||||
(scroll-to-block sibling-block)
|
||||
(util/scroll-to-block sibling-block)
|
||||
(state/exit-editing-and-set-selected-blocks! [sibling-block]))))
|
||||
|
||||
(defn- move-cross-boundary-up-down
|
||||
|
||||
@@ -233,12 +233,20 @@
|
||||
(state/set-state! :editor/on-paste? true)
|
||||
(let [clipboard-data (gobj/get e "clipboardData")
|
||||
html (.getData clipboard-data "text/html")
|
||||
text (.getData clipboard-data "text")]
|
||||
text (.getData clipboard-data "text")
|
||||
has-files? (seq (.-files clipboard-data))]
|
||||
(cond
|
||||
(and (string/blank? text) (string/blank? html))
|
||||
;; When both text and html are blank, paste file if exists.
|
||||
;; NOTE: util/stop is not called here if no file is provided,
|
||||
;; so the default paste behavior of the native platform will be used.
|
||||
(when has-files?
|
||||
(paste-file-if-exists id e))
|
||||
|
||||
;; both file attachment and text/html exist
|
||||
(and has-files? (state/preferred-pasting-file?))
|
||||
(paste-file-if-exists id e)
|
||||
(and (seq (.-files clipboard-data)) (state/preferred-pasting-file?))
|
||||
(paste-file-if-exists id e)
|
||||
|
||||
:else
|
||||
(let [text' (or (when (gp-util/url? text)
|
||||
(wrap-macro-url text))
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
[frontend.handler.route :as route-handler]
|
||||
[frontend.mobile.intent :as intent]
|
||||
[frontend.state :as state]
|
||||
[frontend.util.text :as text-util]))
|
||||
[frontend.util.text :as text-util]
|
||||
[logseq.graph-parser.util :as gp-util]))
|
||||
|
||||
(def *link-to-another-graph (atom false))
|
||||
|
||||
@@ -70,8 +71,14 @@
|
||||
(= hostname "shared")
|
||||
(let [result (into {} (map (fn [key]
|
||||
[(keyword key) (.get search-params key)])
|
||||
["title" "url" "type"]))]
|
||||
(intent/handle-result result))
|
||||
["title" "url" "type" "payload"]))]
|
||||
(if (:payload result)
|
||||
(let [raw (gp-util/safe-decode-uri-component (:payload result))
|
||||
payload (-> raw
|
||||
js/JSON.parse
|
||||
(js->clj :keywordize-keys true))]
|
||||
(intent/handle-payload payload))
|
||||
(intent/handle-result result)))
|
||||
|
||||
:else
|
||||
nil)))
|
||||
|
||||
@@ -152,7 +152,92 @@
|
||||
(gp-util/safe-decode-uri-component v)
|
||||
v))])))
|
||||
|
||||
(defn handle-result [result]
|
||||
(defn- handle-asset-file [url format]
|
||||
(p/let [basename (node-path/basename url)
|
||||
label (-> basename util/node-path.name)
|
||||
path (editor-handler/get-asset-path basename)
|
||||
_file (p/catch
|
||||
(.copy Filesystem (clj->js {:from url :to path}))
|
||||
(fn [error]
|
||||
(log/error :copy-file-error {:error error})))
|
||||
url (util/format "../assets/%s" basename)
|
||||
url-link (editor-handler/get-asset-file-link format url label true)]
|
||||
url-link))
|
||||
|
||||
(defn- handle-payload-resource
|
||||
[{:keys [type name ext url] :as resource} format]
|
||||
(if url
|
||||
(cond
|
||||
(contains? (set/union config/doc-formats config/media-formats)
|
||||
(keyword ext))
|
||||
(handle-asset-file url format)
|
||||
|
||||
:else
|
||||
(notification/show!
|
||||
[:div
|
||||
"Parsing current shared content are not supported. Please report the following codes on "
|
||||
[:a {:href "https://github.com/logseq/logseq/issues/new?labels=from:in-app&template=bug_report.yaml"
|
||||
:target "_blank"} "Github"]
|
||||
". We will look into it soon."
|
||||
[:pre.code (with-out-str (pprint/pprint resource))]] :warning false))
|
||||
|
||||
(cond
|
||||
(= type "text/plain")
|
||||
name
|
||||
|
||||
:else
|
||||
(notification/show!
|
||||
[:div
|
||||
"Parsing current shared content are not supported. Please report the following codes on "
|
||||
[:a {:href "https://github.com/logseq/logseq/issues/new?labels=from:in-app&template=bug_report.yaml"
|
||||
:target "_blank"} "Github"]
|
||||
". We will look into it soon."
|
||||
[:pre.code (with-out-str (pprint/pprint resource))]] :warning false))))
|
||||
|
||||
(defn handle-payload
|
||||
"Mobile share intent handler v2, use complex payload to support more types of content."
|
||||
[payload]
|
||||
;; use :text template, use {url} as rich text placeholder
|
||||
(p/let [page (or (state/get-current-page) (string/lower-case (date/journal-name)))
|
||||
format (db/get-page-format page)
|
||||
|
||||
template (get-in (state/get-config)
|
||||
[:quick-capture-templates :text]
|
||||
"**{time}** [[quick capture]]: {text} {url}")
|
||||
{:keys [text resources]} payload
|
||||
text (or text "")
|
||||
rich-content (-> (p/all (map (fn [resource]
|
||||
(handle-payload-resource resource format))
|
||||
resources))
|
||||
(p/then (partial string/join "\n")))]
|
||||
(when (or (not-empty text) (not-empty rich-content))
|
||||
(let [time (date/get-current-time)
|
||||
date-ref-name (date/today)
|
||||
content (-> template
|
||||
(string/replace "{time}" time)
|
||||
(string/replace "{date}" date-ref-name)
|
||||
(string/replace "{text}" text)
|
||||
(string/replace "{url}" rich-content))
|
||||
edit-content (state/get-edit-content)
|
||||
edit-content-blank? (string/blank? edit-content)
|
||||
edit-content-include-capture? (and (not-empty edit-content)
|
||||
(string/includes? edit-content "[[quick capture]]"))]
|
||||
(if (and (state/editing?) (not edit-content-include-capture?))
|
||||
(if edit-content-blank?
|
||||
(editor-handler/insert content)
|
||||
(editor-handler/insert (str "\n" content)))
|
||||
|
||||
(do
|
||||
(editor-handler/escape-editing)
|
||||
(js/setTimeout #(editor-handler/api-insert-new-block! content {:page page
|
||||
:edit-block? true
|
||||
:replace-empty-target? true})
|
||||
100)))))))
|
||||
|
||||
|
||||
(defn handle-result
|
||||
"Mobile share intent handler v1, legacy. Only for Android"
|
||||
[result]
|
||||
(let [result (decode-received-result result)]
|
||||
(when-let [type (:type result)]
|
||||
(cond
|
||||
|
||||
@@ -417,6 +417,16 @@
|
||||
%))
|
||||
(take-while (complement nil?) (iterate #(.-parentElement %) element)))))
|
||||
|
||||
#?(:cljs
|
||||
(defn element-visible?
|
||||
[element]
|
||||
(when element
|
||||
(when-let [r (.getBoundingClientRect element)]
|
||||
(and (>= (.-top r) 0)
|
||||
(<= (+ (.-bottom r) 64)
|
||||
(or (.-innerHeight js/window)
|
||||
(js/document.documentElement.clientHeight))))))))
|
||||
|
||||
#?(:cljs
|
||||
(defn element-top [elem top]
|
||||
(when elem
|
||||
@@ -467,6 +477,19 @@
|
||||
([animate?]
|
||||
(scroll-to (app-scroll-container-node) 0 animate?))))
|
||||
|
||||
#?(:cljs
|
||||
(defn scroll-to-block
|
||||
"Scroll into the view to vertically align a non-visible block to the centre
|
||||
of the visible area"
|
||||
([block]
|
||||
(scroll-to-block block true))
|
||||
([block animate?]
|
||||
(when block
|
||||
(when-not (element-visible? block)
|
||||
(.scrollIntoView block
|
||||
#js {:behavior (if animate? "smooth" "auto")
|
||||
:block "center"}))))))
|
||||
|
||||
#?(:cljs
|
||||
(defn link?
|
||||
[node]
|
||||
@@ -1416,16 +1439,6 @@
|
||||
(fn [resolve]
|
||||
(load url resolve)))))
|
||||
|
||||
#?(:cljs
|
||||
(defn element-visible?
|
||||
[element]
|
||||
(when element
|
||||
(when-let [r (.getBoundingClientRect element)]
|
||||
(and (>= (.-top r) 0)
|
||||
(<= (+ (.-bottom r) 64)
|
||||
(or (.-innerHeight js/window)
|
||||
(js/document.documentElement.clientHeight))))))))
|
||||
|
||||
#?(:cljs
|
||||
(defn copy-image-to-clipboard
|
||||
[src]
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
(ns ^:no-doc frontend.version)
|
||||
|
||||
(defonce version "0.9.6")
|
||||
(defonce version "0.9.8")
|
||||
|
||||
Reference in New Issue
Block a user