mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-02-23 19:49:56 +01:00
mobile: add home shortcuts
Add ability to create shortcuts on launcher homescreen for notes, notebooks and tags.
This commit is contained in:
@@ -6,7 +6,14 @@ import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.ShortcutInfo;
|
||||
import android.content.pm.ShortcutManager;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.RectF;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.provider.Settings;
|
||||
@@ -237,6 +244,179 @@ public class RCTNNativeModule extends ReactContextBaseJavaModule {
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void addShortcut(final String id, final String type, final String title, final String description, Promise promise) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
promise.reject("UNSUPPORTED", "Pinned launcher shortcuts require Android 8.0 or higher");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
ShortcutManager shortcutManager = mContext.getSystemService(ShortcutManager.class);
|
||||
if (shortcutManager == null) {
|
||||
promise.reject("ERROR", "ShortcutManager not available");
|
||||
return;
|
||||
}
|
||||
|
||||
String uri = "https://app.notesnook.com/open_" + type + "?id=" + id;
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW, android.net.Uri.parse(uri));
|
||||
intent.setPackage(mContext.getPackageName());
|
||||
|
||||
Icon icon = createLetterIcon(type, title);
|
||||
|
||||
ShortcutInfo shortcut = new ShortcutInfo.Builder(mContext, id)
|
||||
.setShortLabel(title)
|
||||
.setLongLabel(description != null && !description.isEmpty() ? description : title)
|
||||
.setIcon(icon)
|
||||
.setIntent(intent)
|
||||
.build();
|
||||
|
||||
shortcutManager.requestPinShortcut(shortcut, null);
|
||||
promise.resolve(true);
|
||||
} catch (Exception e) {
|
||||
promise.reject("ERROR", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void removeShortcut(final String id, Promise promise) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
promise.reject("UNSUPPORTED", "Pinned launcher shortcuts require Android 8.0 or higher");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
ShortcutManager shortcutManager = mContext.getSystemService(ShortcutManager.class);
|
||||
if (shortcutManager == null) {
|
||||
promise.reject("ERROR", "ShortcutManager not available");
|
||||
return;
|
||||
}
|
||||
|
||||
shortcutManager.disableShortcuts(java.util.Collections.singletonList(id));
|
||||
promise.resolve(true);
|
||||
} catch (Exception e) {
|
||||
promise.reject("ERROR", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void updateShortcut(final String id, final String title, final String description, Promise promise) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
promise.reject("UNSUPPORTED", "Pinned launcher shortcuts require Android 8.0 or higher");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
ShortcutManager shortcutManager = mContext.getSystemService(ShortcutManager.class);
|
||||
if (shortcutManager == null) {
|
||||
promise.reject("ERROR", "ShortcutManager not available");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get existing shortcut to preserve icon and intent
|
||||
List<ShortcutInfo> shortcuts = shortcutManager.getPinnedShortcuts();
|
||||
ShortcutInfo existingShortcut = null;
|
||||
for (ShortcutInfo s : shortcuts) {
|
||||
if (s.getId().equals(id)) {
|
||||
existingShortcut = s;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (existingShortcut == null) {
|
||||
promise.reject("NOT_FOUND", "Shortcut not found");
|
||||
return;
|
||||
}
|
||||
|
||||
ShortcutInfo updatedShortcut = new ShortcutInfo.Builder(mContext, id)
|
||||
.setShortLabel(title)
|
||||
.setLongLabel(description != null && !description.isEmpty() ? description : title)
|
||||
.setIntent(existingShortcut.getIntent())
|
||||
.build();
|
||||
|
||||
shortcutManager.updateShortcuts(java.util.Collections.singletonList(updatedShortcut));
|
||||
promise.resolve(true);
|
||||
} catch (Exception e) {
|
||||
promise.reject("ERROR", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void removeAllShortcuts(Promise promise) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
promise.reject("UNSUPPORTED", "Pinned launcher shortcuts require Android 8.0 or higher");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
ShortcutManager shortcutManager = mContext.getSystemService(ShortcutManager.class);
|
||||
if (shortcutManager == null) {
|
||||
promise.reject("ERROR", "ShortcutManager not available");
|
||||
return;
|
||||
}
|
||||
|
||||
List<ShortcutInfo> pinnedShortcuts = shortcutManager.getPinnedShortcuts();
|
||||
if (!pinnedShortcuts.isEmpty()) {
|
||||
List<String> ids = new ArrayList<>();
|
||||
for (ShortcutInfo shortcut : pinnedShortcuts) {
|
||||
ids.add(shortcut.getId());
|
||||
}
|
||||
shortcutManager.disableShortcuts(ids);
|
||||
}
|
||||
promise.resolve(true);
|
||||
} catch (Exception e) {
|
||||
promise.reject("ERROR", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private Icon createLetterIcon(String type, String title) {
|
||||
|
||||
String letter = type.contains("tag") ? "#" : title != null && !title.isEmpty()
|
||||
? title.substring(0, 1).toUpperCase()
|
||||
: "?";
|
||||
int color = getColorForLetter(letter);
|
||||
|
||||
if (type.equals("notebook")) return Icon.createWithResource(mContext, R.drawable.ic_notebook);
|
||||
// Use a larger canvas and fill it completely to avoid white borders from launcher masking.
|
||||
int iconSize = 256;
|
||||
Bitmap bitmap = Bitmap.createBitmap(iconSize, iconSize, Bitmap.Config.ARGB_8888);
|
||||
Canvas canvas = new Canvas(bitmap);
|
||||
|
||||
|
||||
|
||||
Paint textPaint = new Paint();
|
||||
textPaint.setColor(color);
|
||||
textPaint.setTextSize(130);
|
||||
textPaint.setAntiAlias(true);
|
||||
textPaint.setTextAlign(Paint.Align.CENTER);
|
||||
textPaint.setTypeface(android.graphics.Typeface.create(android.graphics.Typeface.DEFAULT, android.graphics.Typeface.BOLD));
|
||||
|
||||
float x = iconSize / 2f;
|
||||
float y = (iconSize / 2f) - ((textPaint.descent() + textPaint.ascent()) / 2f);
|
||||
|
||||
canvas.drawText(letter, x, y, textPaint);
|
||||
|
||||
return Icon.createWithBitmap(bitmap);
|
||||
}
|
||||
|
||||
private int getColorForLetter(String letter) {
|
||||
int[] colors = {
|
||||
0xFF1976D2, // Blue
|
||||
0xFFD32F2F, // Red
|
||||
0xFF388E3C, // Green
|
||||
0xFFF57C00, // Orange
|
||||
0xFF7B1FA2, // Purple
|
||||
0xFF0097A7, // Cyan
|
||||
0xFFC2185B, // Pink
|
||||
0xFF455A64, // Blue Grey
|
||||
0xFF6A1B9A, // Deep Purple
|
||||
0xFF00796B, // Teal
|
||||
0xFF512DA8, // Indigo
|
||||
0xFF1565C0 // Dark Blue
|
||||
};
|
||||
|
||||
int hash = Math.abs(letter.hashCode());
|
||||
return colors[hash % colors.length];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="#008837"
|
||||
android:alpha="1">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M240,880Q207,880 183.5,856.5Q160,833 160,800L160,160Q160,127 183.5,103.5Q207,80 240,80L720,80Q753,80 776.5,103.5Q800,127 800,160L800,800Q800,833 776.5,856.5Q753,880 720,880L240,880ZM240,800L720,800Q720,800 720,800Q720,800 720,800L720,160Q720,160 720,160Q720,160 720,160L640,160L640,440L540,380L440,440L440,160L240,160Q240,160 240,160Q240,160 240,160L240,800Q240,800 240,800Q240,800 240,800ZM240,800Q240,800 240,800Q240,800 240,800L240,160Q240,160 240,160Q240,160 240,160L240,160Q240,160 240,160Q240,160 240,160L240,800Q240,800 240,800Q240,800 240,800ZM440,440L540,380L640,440L640,440L540,380L440,440Z"/>
|
||||
</vector>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 284 B |
Binary file not shown.
|
After Width: | Height: | Size: 211 B |
Binary file not shown.
|
After Width: | Height: | Size: 440 B |
Binary file not shown.
|
After Width: | Height: | Size: 807 B |
@@ -57,6 +57,7 @@ const BOTTOM_BAR_ITEMS: ActionId[] = [
|
||||
"export",
|
||||
"copy-link",
|
||||
"duplicate",
|
||||
"launcher-shortcut",
|
||||
"trash"
|
||||
];
|
||||
|
||||
@@ -76,6 +77,7 @@ const COLUMN_BAR_ITEMS: ActionId[] = [
|
||||
"reorder",
|
||||
"rename-color",
|
||||
"rename-tag",
|
||||
"launcher-shortcut",
|
||||
"restore",
|
||||
"trash",
|
||||
"delete"
|
||||
@@ -170,15 +172,15 @@ export const Items = ({
|
||||
DDS.isTab
|
||||
? AppFontSize.xxl
|
||||
: shouldShrink
|
||||
? AppFontSize.xxl
|
||||
: AppFontSize.lg
|
||||
? AppFontSize.xxl
|
||||
: AppFontSize.lg
|
||||
}
|
||||
color={
|
||||
item.checked
|
||||
? item.activeColor || colors.primary.accent
|
||||
: item.id.match(/(delete|trash)/g)
|
||||
? colors.error.icon
|
||||
: colors.secondary.icon
|
||||
? colors.error.icon
|
||||
: colors.secondary.icon
|
||||
}
|
||||
/>
|
||||
</Pressable>
|
||||
@@ -209,8 +211,8 @@ export const Items = ({
|
||||
text: item.checked
|
||||
? item.activeColor || colors.primary.accent
|
||||
: item.id === "delete" || item.id === "trash"
|
||||
? colors.error.paragraph
|
||||
: colors.primary.paragraph
|
||||
? colors.error.paragraph
|
||||
: colors.primary.paragraph
|
||||
}}
|
||||
testID={"icon-" + item.id}
|
||||
onPress={item.onPress}
|
||||
@@ -274,8 +276,8 @@ export const Items = ({
|
||||
item.checked
|
||||
? item.activeColor || colors.primary.accent
|
||||
: item.id === "delete" || item.id === "trash"
|
||||
? colors.error.icon
|
||||
: colors.secondary.icon
|
||||
? colors.error.icon
|
||||
: colors.secondary.icon
|
||||
}
|
||||
/>
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
Item,
|
||||
ItemReference,
|
||||
Note,
|
||||
Notebook,
|
||||
VAULT_ERRORS
|
||||
} from "@notesnook/core";
|
||||
import { strings } from "@notesnook/intl";
|
||||
@@ -70,6 +71,7 @@ import { deleteItems } from "../utils/functions";
|
||||
import { convertNoteToText } from "../utils/note-to-text";
|
||||
import { sleep } from "../utils/time";
|
||||
import { resetStoredState } from "./use-stored-state";
|
||||
import { NotesnookModule } from "../utils/notesnook-module";
|
||||
|
||||
export type ActionId =
|
||||
| "select"
|
||||
@@ -114,7 +116,8 @@ export type ActionId =
|
||||
| "remove-from-notebook"
|
||||
| "trash"
|
||||
| "default-homepage"
|
||||
| "default-tag";
|
||||
| "default-tag"
|
||||
| "launcher-shortcut";
|
||||
|
||||
export type Action = {
|
||||
id: ActionId;
|
||||
@@ -1207,5 +1210,25 @@ export const useActions = ({
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
Platform.OS === "android" &&
|
||||
(item.type === "tag" || item.type === "note" || item.type === "notebook")
|
||||
) {
|
||||
actions.push({
|
||||
id: "launcher-shortcut",
|
||||
title: strings.addToHome(),
|
||||
icon: "cellphone-arrow-down",
|
||||
onPress: async () => {
|
||||
const added = await NotesnookModule.addShortcut(
|
||||
item.id,
|
||||
item.type,
|
||||
item.title,
|
||||
(item as Note).headline || (item as Notebook).description || ""
|
||||
);
|
||||
console.log(added);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return actions;
|
||||
};
|
||||
|
||||
@@ -171,7 +171,7 @@ const onAppOpenedFromURL = async (event: { url: string }) => {
|
||||
eSendEvent(eOnLoadNote, { newNote: true });
|
||||
fluidTabsRef.current?.goToPage("editor", false);
|
||||
return;
|
||||
} else if (url.startsWith("https://app.notesnook.com/open_note")) {
|
||||
} else if (url.startsWith("https://app.notesnook.com/open_note?")) {
|
||||
const id = new URL(url).searchParams.get("id");
|
||||
if (id) {
|
||||
const note = await db.notes.note(id);
|
||||
@@ -182,6 +182,26 @@ const onAppOpenedFromURL = async (event: { url: string }) => {
|
||||
fluidTabsRef.current?.goToPage("editor", false);
|
||||
}
|
||||
}
|
||||
} else if (url.startsWith("https://app.notesnook.com/open_notebook?")) {
|
||||
const id = new URL(url).searchParams.get("id");
|
||||
if (id) {
|
||||
const notebook = await db.notebooks.notebook(id);
|
||||
if (notebook) {
|
||||
Navigation.navigate("Notebook", {
|
||||
item: notebook
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (url.startsWith("https://app.notesnook.com/open_tag?")) {
|
||||
const id = new URL(url).searchParams.get("id");
|
||||
if (id) {
|
||||
const tag = await db.tags.tag(id);
|
||||
if (tag) {
|
||||
Navigation.navigate("TaggedNotes", {
|
||||
item: tag
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (url.startsWith("https://app.notesnook.com/open_reminder")) {
|
||||
const id = new URL(url).searchParams.get("id");
|
||||
if (id) {
|
||||
@@ -774,23 +794,21 @@ export const useAppEvents = () => {
|
||||
}
|
||||
};
|
||||
|
||||
if (!refValues.current.initialUrl) {
|
||||
Linking.getInitialURL().then((url) => {
|
||||
if (url) {
|
||||
refValues.current.initialUrl = url;
|
||||
}
|
||||
});
|
||||
}
|
||||
let sub: NativeEventSubscription;
|
||||
if (!isAppLoading && !appLocked) {
|
||||
if (!refValues.current.initialUrl) {
|
||||
Linking.getInitialURL().then((url) => {
|
||||
if (url) {
|
||||
refValues.current.initialUrl = url;
|
||||
onAppOpenedFromURL({
|
||||
url: refValues.current.initialUrl!
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
sub = AppState.addEventListener("change", onAppStateChanged);
|
||||
if (refValues.current.initialUrl) {
|
||||
onAppOpenedFromURL({
|
||||
url: refValues.current.initialUrl!
|
||||
});
|
||||
refValues.current.initialUrl = undefined;
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
refValues.current.removeInternetStateListener = NetInfo.addEventListener(
|
||||
|
||||
@@ -1088,7 +1088,10 @@ export const useEditor = (
|
||||
|
||||
if (!state.current?.initialLoadCalled) {
|
||||
const url = await Linking.getInitialURL();
|
||||
let noteId = url && new URL(url).searchParams.get("id");
|
||||
let noteId =
|
||||
url &&
|
||||
url.startsWith("https://app.notesnook.com/open_note?") &&
|
||||
new URL(url).searchParams.get("id");
|
||||
if (noteId) {
|
||||
const note = await db.notes?.note(noteId);
|
||||
fluidTabsRef.current?.goToPage("editor");
|
||||
|
||||
@@ -41,6 +41,19 @@ interface NotesnookModuleInterface {
|
||||
updateWidgetNote: (noteId: string, data: string) => void;
|
||||
updateReminderWidget: () => void;
|
||||
isGestureNavigationEnabled: () => boolean;
|
||||
addShortcut: (
|
||||
id: string,
|
||||
type: "note" | "notebook" | "tag",
|
||||
title: string,
|
||||
description?: string
|
||||
) => Promise<boolean>;
|
||||
removeShortcut: (id: string) => Promise<boolean>;
|
||||
updateShortcut: (
|
||||
id: string,
|
||||
title: string,
|
||||
description?: string
|
||||
) => Promise<boolean>;
|
||||
removeAllShortcuts: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
export const NotesnookModule: NotesnookModuleInterface = Platform.select({
|
||||
@@ -61,7 +74,11 @@ export const NotesnookModule: NotesnookModuleInterface = Platform.select({
|
||||
hasWidgetNote: () => {},
|
||||
updateWidgetNote: () => {},
|
||||
updateReminderWidget: () => {},
|
||||
isGestureNavigationEnabled: () => true
|
||||
isGestureNavigationEnabled: () => true,
|
||||
addShortcut: () => Promise.resolve(false),
|
||||
removeShortcut: () => Promise.resolve(false),
|
||||
updateShortcut: () => Promise.resolve(false),
|
||||
removeAllShortcuts: () => Promise.resolve(false)
|
||||
},
|
||||
android: NativeModules.NNativeModule
|
||||
});
|
||||
|
||||
@@ -115,7 +115,8 @@ const EXTRA_ICON_NAMES = [
|
||||
"update",
|
||||
"notebook-minus",
|
||||
"calendar-blank",
|
||||
"email-newsletter"
|
||||
"email-newsletter",
|
||||
"cellphone-arrow-down"
|
||||
];
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
|
||||
@@ -735,6 +735,10 @@ msgstr "Add tags to multiple notes at once"
|
||||
msgid "Add to dictionary"
|
||||
msgstr "Add to dictionary"
|
||||
|
||||
#: src/strings.ts:2613
|
||||
msgid "Add to home"
|
||||
msgstr "Add to home"
|
||||
|
||||
#: src/strings.ts:2476
|
||||
msgid "Add to notebook"
|
||||
msgstr "Add to notebook"
|
||||
|
||||
@@ -735,6 +735,10 @@ msgstr ""
|
||||
msgid "Add to dictionary"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2613
|
||||
msgid "Add to home"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2476
|
||||
msgid "Add to notebook"
|
||||
msgstr ""
|
||||
|
||||
@@ -2609,5 +2609,6 @@ Use this if changes from other devices are not appearing on this device. This wi
|
||||
views: () => t`Views`,
|
||||
clickToUpdate: () => t`Click to update`,
|
||||
noPassword: () => t`No password`,
|
||||
publishToTheWeb: () => t`Publish to the web`
|
||||
publishToTheWeb: () => t`Publish to the web`,
|
||||
addToHome: () => t`Add to home`
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user