Merge branch 'master' into enhance/keymaps-manager-x

This commit is contained in:
Gabriel Horner
2023-05-25 11:12:03 -04:00
committed by GitHub
18 changed files with 690 additions and 171 deletions

View File

@@ -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

View File

@@ -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.

View File

@@ -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;

View File

@@ -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')
})
}
})

View File

@@ -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;
}

View File

@@ -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)";

View File

@@ -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>

View File

@@ -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
}
}

View 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 = []
}
}

View File

@@ -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",

View File

@@ -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 "Открыть файл в родительском каталоге"

View File

@@ -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])))

View File

@@ -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

View File

@@ -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))

View File

@@ -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)))

View File

@@ -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

View File

@@ -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]

View File

@@ -1,3 +1,3 @@
(ns ^:no-doc frontend.version)
(defonce version "0.9.6")
(defonce version "0.9.8")