mobile: update ui

This commit is contained in:
Ammar Ahmed
2026-07-01 13:45:18 +05:00
parent 0f0d239e11
commit 317f889569
18 changed files with 313 additions and 244 deletions

View File

@@ -32,7 +32,6 @@ import { useSelectionStore } from "../../stores/use-selection-store";
import { useSettingStore } from "../../stores/use-setting-store";
import { eScrollEvent } from "../../utils/events";
import { fluidTabsRef } from "../../utils/global-refs";
import { DefaultAppStyles } from "../../utils/styles";
import { IconButton, IconButtonProps } from "../ui/icon-button";
import Heading from "../ui/typography/heading";
@@ -106,7 +105,7 @@ export const Header = ({
return (
<View
style={{
paddingHorizontal: DefaultAppStyles.GAP,
paddingHorizontal: Spacing.LEVEL_3,
marginTop: Platform.OS === "android" ? 5 : 0
}}
>
@@ -116,7 +115,6 @@ export const Header = ({
flexDirection: "row",
justifyContent: "space-between",
borderRadius: Radius.S,
paddingHorizontal: Spacing.LEVEL_2,
paddingVertical: Spacing.LEVEL_3,
backgroundColor: colors.secondary.background,
alignItems: "center"

View File

@@ -162,9 +162,9 @@ const NoteItem = ({
);
const canShowTopStatusBar =
attachmentsCount > 0 &&
activeStatusIcons.length > 0 &&
notebooks?.items?.length &&
attachmentsCount > 0 ||
activeStatusIcons.length > 0 ||
notebooks?.items?.length ||
tags?.items.length;
return (
@@ -369,7 +369,7 @@ const NoteItem = ({
gap: Spacing.LEVEL_1
}}
>
{color ? (
{color && compactMode ? (
<View
style={{
width: 8,

View File

@@ -89,7 +89,9 @@ const SelectionWrapper = ({
)}
<View
style={{
backgroundColor: undefined
backgroundColor: isEditingNote
? colors.selected.background
: undefined
}}
>
<Pressable

View File

@@ -27,6 +27,7 @@ import { View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { notesnook } from "../../../e2e/test.ids";
import { db } from "../../common/database";
import { Spacing } from "../../common/design/spacing";
import {
eSendEvent,
sendItemUpdateEvent,
@@ -38,13 +39,10 @@ import { useRelationStore } from "../../stores/use-relation-store";
import { useSettingStore } from "../../stores/use-setting-store";
import { refreshNotesPage } from "../../utils/events";
import { AppFontSize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import ColorPicker from "../dialogs/color-picker";
import PaywallSheet from "../sheets/paywall";
import { Button } from "../ui/button";
import { Pressable } from "../ui/pressable";
import { Spacing } from "../../common/design/spacing";
import AppIcon from "../ui/AppIcon";
import { Pressable } from "../ui/pressable";
const ColorItem = ({ item, note }: { item: Color; note: Note }) => {
const { colors } = useThemeColors();
@@ -86,15 +84,17 @@ const ColorItem = ({ item, note }: { item: Color; note: Note }) => {
height: 40,
borderRadius: 100,
justifyContent: "center",
alignItems: "center"
alignItems: "center",
borderWidth: 0
}}
>
{isLinked ? (
<Icon
<AppIcon
testID="icon-check"
name="check"
color="white"
size={AppFontSize.lg}
name="check-small"
iconFamily="notesnook"
color={colors.static.black}
size={15}
/>
) : null}
</Pressable>

View File

@@ -97,7 +97,7 @@ export const DateMeta = ({ item }: { item: Item }) => {
{key === "dateCreated" && item.type === "note" ? (
<>
<AppIcon name="edit-pencil" size={10} iconFamily="notesnook" />
<AppIcon name="edit-pencil" size={16} iconFamily="notesnook" />
</>
) : null}
</View>

View File

@@ -63,6 +63,7 @@ import {
Tag,
TrashItem
} from "@notesnook/core";
import LineSeparator from "../ui/seperator/line-separator";
export type PropertiesItem =
| Note
@@ -131,7 +132,7 @@ export const Properties = ({
borderBottomRightRadius: DDS.isLargeTablet() ? 10 : 1,
borderBottomLeftRadius: DDS.isLargeTablet() ? 10 : 1,
maxHeight: "100%",
paddingTop: Spacing.LEVEL_2
paddingTop: Spacing.LEVEL_3
}}
nestedScrollEnabled
bounces={false}
@@ -159,8 +160,7 @@ export const Properties = ({
>
<View
style={{
gap: Spacing.LEVEL_1,
paddingBottom: Spacing.LEVEL_3
gap: Spacing.LEVEL_1
}}
>
{item.type === "note" && (noteNotebooks.length || tags.length) ? (
@@ -248,7 +248,7 @@ export const Properties = ({
/>
) : null}
<Heading size={AppFontSize.xl}>{item.title}</Heading>
<Heading fontSize="XL">{item.title}</Heading>
{editAction ? (
<IconButton
@@ -306,92 +306,95 @@ export const Properties = ({
/>
) : null}
</View>
<DateMeta item={item} />
{item.type === "note" && colorNotes.length > 0 ? (
<Tags close={close} item={item} />
) : null}
{item.type === "note" ? (
<View
style={{
flexDirection: "row",
borderBottomWidth: 1,
borderTopWidth: colorNotes.length > 0 ? 1 : 0,
borderColor: colors.primary.border,
paddingVertical: Spacing.LEVEL_2,
paddingTop: colorNotes.length > 0 ? Spacing.LEVEL_2 : 0,
gap: Spacing.LEVEL_2
}}
>
<Button
onPress={async () => {
ManageTags.present([item.id]);
close?.();
}}
buttonType={{
text: colors.primary.paragraph
}}
title={strings.addTag()}
type={colorNotes?.length ? "accent-outline" : "shade"}
icon="plus"
iconFamily="notesnook"
<LineSeparator paddingVertical={Spacing.LEVEL_3} />
) : null}
<DateMeta item={item} />.
{item.type === "note" && colorNotes.length > 0 ? (
<View>
<LineSeparator
paddingVertical={Spacing.LEVEL_3}
style={{
paddingHorizontal: Spacing.LEVEL_3,
paddingVertical: Spacing.LEVEL_2,
width: colorNotes?.length > 0 ? "100%" : "48.5%"
paddingTop: Spacing.LEVEL_2
}}
/>
{colorNotes.length > 0 ? null : (
<Tags close={close} item={item} />
<LineSeparator
paddingVertical={Spacing.LEVEL_3}
style={{
paddingBottom: 0
}}
/>
</View>
) : null}
{item.type === "note" ? (
<>
<View
style={{
flexDirection: "row",
gap: Spacing.LEVEL_2,
marginTop: Spacing.LEVEL_2
}}
>
<Button
onPress={() => {
if (colorFeature && !colorFeature.isAllowed) {
ToastManager.show({
message: colorFeature.error,
type: "info",
context: "local",
actionText: strings.upgrade(),
func: () => {
PaywallSheet.present(colorFeature);
ToastManager.hide();
}
});
return;
}
useSettingStore.getState().setSheetKeyboardHandler(false);
setVisible(true);
onPress={async () => {
ManageTags.present([item.id]);
close?.();
}}
title={strings.addColor()}
type="secondaryAccented"
title={
colorNotes?.length
? strings.addTag()
: strings.dataTypesCamelCase.tag()
}
type={colorNotes?.length ? "accent-outline" : "shade-plain"}
icon="plus"
iconFamily="notesnook"
style={{
width: "48.5%",
paddingHorizontal: Spacing.LEVEL_3,
paddingVertical: Spacing.LEVEL_2
paddingVertical: Spacing.LEVEL_2,
width: colorNotes?.length > 0 ? "100%" : "48.5%"
}}
/>
)}
</View>
{colorNotes.length > 0 ? null : (
<Button
onPress={() => {
if (colorFeature && !colorFeature.isAllowed) {
ToastManager.show({
message: colorFeature.error,
type: "info",
context: "local",
actionText: strings.upgrade(),
func: () => {
PaywallSheet.present(colorFeature);
ToastManager.hide();
}
});
return;
}
useSettingStore.getState().setSheetKeyboardHandler(false);
setVisible(true);
}}
title={strings.dataTypesCamelCase.color()}
type="shade-plain"
icon="plus"
iconFamily="notesnook"
style={{
width: "48.5%",
paddingHorizontal: Spacing.LEVEL_3,
paddingVertical: Spacing.LEVEL_2
}}
/>
)}
</View>
</>
) : null}
</View>
<View
style={{
paddingHorizontal: Spacing.LEVEL_3
}}
>
<View
style={{
marginVertical: Spacing.LEVEL_3,
width: "100%",
borderBottomWidth: 1,
borderColor: colors.primary.separator
}}
/>
</View>
<LineSeparator
paddingVertical={Spacing.LEVEL_2}
paddingHorizontal={Spacing.LEVEL_3}
/>
<Items
item={item}

View File

@@ -36,10 +36,7 @@ export const Tags = ({ item, close }) => {
<View
style={{
flexDirection: "row",
width: "100%",
borderTopWidth: 1,
borderColor: colors.primary.border,
paddingVertical: Spacing.LEVEL_3
width: "100%"
}}
>
<ColorTags item={item} />

View File

@@ -285,7 +285,16 @@ const Sort = ({
await updateGroupOptions(_groupOptions);
}}
>
<Paragraph>
<Paragraph
color={
groupOptions?.sortBy === item
? colors.primary.heading
: colors.primary.paragraph
}
fontFamily={
groupOptions?.sortBy === item ? "MEDIUM" : "REGULAR"
}
>
{strings.sortByStrings[
item as keyof typeof strings.sortByStrings
]()}

View File

@@ -188,7 +188,7 @@ const buttonTypes = (
"accent-outline": {
primary: "transparent",
text: colors.primary.accent,
selected: accent || colors.primary.accent,
selected: "transparent",
borderWidth: 1,
borderColor: accent || colors.primary.accent,
borderSelectedColor: accent || colors.primary.accent
@@ -217,9 +217,9 @@ const buttonTypes = (
text: colors.primary.heading,
selected: colors.primary.accent,
colorOpacity: 0.12,
borderWidth: 0.8,
borderColor: getColorLinearShade(colors.primary.shade, 0.3, isDark),
borderSelectedColor: getColorLinearShade(colors.primary.shade, 0.3, isDark)
borderWidth: 0
// borderColor: getColorLinearShade(colors.primary.shade, 0.3, isDark),
// borderSelectedColor: getColorLinearShade(colors.primary.shade, 0.3, isDark)
},
error: {
primary: colors.error.background,

View File

@@ -25,17 +25,19 @@ import { useThemeColors } from "@notesnook/theme";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { TextInput, View } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../common/database";
import { Radius, Spacing } from "../../common/design/spacing";
import { Header } from "../../components/header";
import AppIcon from "../../components/ui/AppIcon";
import { Button } from "../../components/ui/button";
import Input from "../../components/ui/input";
import { Pressable } from "../../components/ui/pressable";
import Heading from "../../components/ui/typography/heading";
import Paragraph from "../../components/ui/typography/paragraph";
import { useNavigationFocus } from "../../hooks/use-navigation-focus";
import {
sendItemUpdateEvent,
ToastManager
ToastManager,
sendItemUpdateEvent
} from "../../services/event-manager";
import Navigation, { NavigationProps } from "../../services/navigation";
import {
@@ -44,8 +46,7 @@ import {
} from "../../stores/item-selection-store";
import { useRelationStore } from "../../stores/use-relation-store";
import { useTagStore } from "../../stores/use-tag-store";
import { AppFontSize, defaultBorderRadius } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import { AppFontSize } from "../../utils/size";
async function updateInitialSelectionState(items: string[]) {
const relations = await db.relations
@@ -90,18 +91,26 @@ const ManageTags = (props: NavigationProps<"ManageTags">) => {
const [tags, setTags] = useState<Tag[]>();
const [query, setQuery] = useState<string>();
const inputRef = useRef<TextInput>(null);
useNavigationFocus(props.navigation, { focusOnInit: true });
useNavigationFocus(props.navigation, {
focusOnInit: true
});
const timerRef = useRef<NodeJS.Timeout>(undefined);
const [queryExists, setQueryExists] = useState(false);
const refreshSelection = useCallback(() => {
updateInitialSelectionState(ids).then((selection) => {
updateInitialSelectionState(ids).then((initialState) => {
const prev = useTagItemSelection.getState();
const selection = { ...initialState };
for (const id in prev.selection) {
if (prev.selection[id] !== prev.initialState[id]) {
selection[id] = prev.selection[id];
}
}
useTagItemSelection.setState({
initialState: selection,
selection: { ...selection }
initialState,
selection
});
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ids, tags]);
}, [ids]);
const sortAndSetTags = useCallback(
async (items: VirtualizedGrouping<Tag>) => {
@@ -130,6 +139,12 @@ const ManageTags = (props: NavigationProps<"ManageTags">) => {
[ids]
);
useEffect(() => {
return () => {
useTagItemSelection.getState().reset();
};
}, []);
const refreshTags = useCallback(() => {
if (query && query.trim() !== "") {
db.lookup
@@ -190,109 +205,128 @@ const ManageTags = (props: NavigationProps<"ManageTags">) => {
}
if (id) {
for (const noteId of ids) {
await db.relations.add(
{
id: id,
type: "tag"
},
{
id: noteId,
type: "note"
}
);
}
const { selection } = useTagItemSelection.getState();
useTagItemSelection.setState({
selection: {
...selection,
[id]: "selected"
}
});
}
useRelationStore.getState().update();
sendItemUpdateEvent(id, "tag");
useTagStore.getState().refresh();
setQuery(undefined);
} catch (e) {
ToastManager.error(e as Error);
}
Navigation.queueRoutesForUpdate();
};
const onPress = useCallback(
async (id: string) => {
for (const noteId of ids) {
try {
if (!id) return;
const isSelected =
useTagItemSelection.getState().initialState[id] === "selected";
if (isSelected) {
await db.relations.unlink(
{
id: id,
type: "tag"
},
{
id: noteId,
type: "note"
}
const onPress = useCallback((id: string) => {
if (!id) return;
const { selection } = useTagItemSelection.getState();
useTagItemSelection.setState({
selection: {
...selection,
[id]: selection[id] === "selected" ? "deselected" : "selected"
}
});
}, []);
const onSave = useCallback(async () => {
const { selection, initialState } = useTagItemSelection.getState();
const changedIds = new Set([
...Object.keys(selection),
...Object.keys(initialState)
]);
try {
for (const id of changedIds) {
if (selection[id] === initialState[id]) continue;
const shouldLink = selection[id] === "selected";
for (const noteId of ids) {
if (shouldLink) {
await db.relations.add(
{ id, type: "tag" },
{ id: noteId, type: "note" }
);
} else {
await db.relations.add(
{
id: id,
type: "tag"
},
{
id: noteId,
type: "note"
}
await db.relations.unlink(
{ id, type: "tag" },
{ id: noteId, type: "note" }
);
}
} catch (e) {
console.error(e);
}
sendItemUpdateEvent(id, "tag");
}
sendItemUpdateEvent(id, "tag");
useTagStore.getState().refresh();
useRelationStore.getState().update();
refreshTags();
setTimeout(() => {
Navigation.queueRoutesForUpdate();
}, 1);
refreshSelection();
},
[ids, refreshSelection, refreshTags]
);
Navigation.queueRoutesForUpdate();
Navigation.goBack();
} catch (e) {
ToastManager.error(e as Error);
}
}, [ids]);
const renderTag = useCallback(
({ index, item }: { item: Tag; index: number }) => (
({ item }: { item: Tag; index: number }) => (
<TagItem tag={item} onPress={onPress} />
),
[onPress]
);
const hasChanges = useTagItemSelection((state) => {
const norm = (v?: string) =>
v === "selected" ? "s" : v === "intermediate" ? "i" : "n";
const changedIds = new Set([
...Object.keys(state.selection),
...Object.keys(state.initialState)
]);
for (const id of changedIds) {
if (norm(state.selection[id]) !== norm(state.initialState[id])) {
return true;
}
}
return false;
});
return (
<SafeAreaView
style={{
width: "100%",
alignSelf: "center",
backgroundColor: colors.primary.background,
gap: DefaultAppStyles.GAP_VERTICAL,
flex: 1
}}
>
<Header title={strings.manageTags()} canGoBack />
<Header
style={{
backgroundColor: "transparent"
}}
title={strings.manageTags()}
canGoBack
/>
<View
style={{
paddingHorizontal: DefaultAppStyles.GAP,
flex: 1
paddingHorizontal: Spacing.LEVEL_3,
flex: 1,
gap: Spacing.LEVEL_3
}}
>
<Input
button={{
icon: "magnify",
color: colors.primary.accent,
size: AppFontSize.lg,
icon: "search",
iconFamily: "notesnook",
color: colors.primary.icon,
size: 16,
onPress: () => {}
}}
containerStyle={{
backgroundColor: colors.secondary.background,
borderWidth: 0
}}
testID="tag-input"
fwdRef={inputRef}
autoCapitalize="none"
@@ -309,28 +343,12 @@ const ManageTags = (props: NavigationProps<"ManageTags">) => {
placeholder={strings.searchForTags()}
/>
{query && !queryExists ? (
<Pressable
key={"query_item"}
style={{
flexDirection: "row",
justifyContent: "space-between",
paddingHorizontal: DefaultAppStyles.GAP,
paddingVertical: DefaultAppStyles.GAP_VERTICAL
}}
onPress={onSubmit}
type="selected"
>
<Heading size={AppFontSize.sm} color={colors.selected.heading}>
{strings.add()} {'"' + "#" + query + '"'}
</Heading>
<Icon
name="plus"
color={colors.selected.icon}
size={AppFontSize.lg}
/>
</Pressable>
) : null}
<View
style={{
height: 1,
backgroundColor: colors.primary.border
}}
/>
<View
style={{
@@ -344,6 +362,37 @@ const ManageTags = (props: NavigationProps<"ManageTags">) => {
keyboardDismissMode="interactive"
estimatedItemSize={50}
renderItem={renderTag}
ListHeaderComponent={
query && !queryExists ? (
<Pressable
key={"query_item"}
style={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
padding: Spacing.LEVEL_2,
borderRadius: Radius.XS,
marginBottom: Spacing.LEVEL_0
}}
onPress={onSubmit}
type="selected"
>
<Heading
fontSize="SM"
fontFamily="MEDIUM"
color={colors.selected.heading}
>
{strings.add()} {'"' + "#" + query + '"'}
</Heading>
<AppIcon
name="plus"
iconFamily="material"
color={colors.selected.icon}
size={AppFontSize.md}
/>
</Pressable>
) : null
}
ListEmptyComponent={
<View
style={{
@@ -368,6 +417,23 @@ const ManageTags = (props: NavigationProps<"ManageTags">) => {
/>
</View>
</View>
<View
style={{
borderTopWidth: 1,
borderTopColor: colors.primary.border,
backgroundColor: colors.primary.background,
padding: Spacing.LEVEL_3
}}
>
<Button
title={strings.saveChanges()}
type="accent"
width="100%"
disabled={!hasChanges}
onPress={onSave}
/>
</View>
</SafeAreaView>
);
};
@@ -389,65 +455,49 @@ const TagItem = ({
}) => {
const { colors } = useThemeColors();
const selection = useTagItemSelection((state) =>
tag?.id ? state.selection[tag?.id] : false
tag?.id ? state.selection[tag?.id] : undefined
);
const selected = selection === "selected" || selection === "intermediate";
return (
<Pressable
style={{
flexDirection: "row",
marginVertical: 5,
alignItems: "center",
gap: Spacing.LEVEL_1,
justifyContent: "flex-start",
height: 40
padding: Spacing.LEVEL_2,
borderRadius: Radius.XS,
marginBottom: Spacing.LEVEL_0
}}
onPress={() => {
if (!tag) return;
onPress(tag.id);
}}
type="plain"
type={selected ? "selected" : "transparent"}
>
{!tag ? null : (
<Icon
size={22}
onPress={() => {
if (!tag) return;
onPress(tag.id);
}}
color={
selection === "selected" || selection === "intermediate"
? colors.selected.icon
: colors.primary.icon
}
style={{
marginRight: 6
}}
testID={
selection === "selected"
? "check-circle-outline"
: selection === "intermediate"
? "minus-circle-outline"
: "checkbox-blank-circle-outline"
}
name={
selection === "selected"
? "check-circle-outline"
: selection === "intermediate"
? "minus-circle-outline"
: "checkbox-blank-circle-outline"
}
/>
)}
{tag ? (
<Paragraph size={AppFontSize.sm}>{"#" + tag?.title}</Paragraph>
) : (
<View
style={{
width: 200,
height: 30,
borderRadius: defaultBorderRadius
}}
/>
)}
<AppIcon
name={
selection === "selected"
? "checkbox"
: selection === "intermediate"
? "checkbox-intermediate"
: "box-empty"
}
iconFamily="notesnook"
size={16}
color={
selected
? [colors.selected.accent, colors.static.white]
: colors.secondary.icon
}
/>
<Paragraph
fontSize="SM"
color={selected ? colors.primary.heading : colors.secondary.paragraph}
>
{"#" + tag?.title}
</Paragraph>
</Pressable>
);
};

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Design Icons by Pictogrammers - https://github.com/Templarian/MaterialDesign/blob/master/LICENSE --><path fill="currentColor" d="M19 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2m0 16H5V5h14zm-2-2H7V7h10z"/></svg>

After

Width:  |  Height:  |  Size: 342 B

View File

@@ -6209,6 +6209,10 @@ msgstr "Save account recovery key"
msgid "Save and continue"
msgstr "Save and continue"
#: src/strings.ts:2942
msgid "Save changes"
msgstr "Save changes"
#: src/strings.ts:1066
msgid "Save data recovery key"
msgstr "Save data recovery key"

View File

@@ -6183,6 +6183,10 @@ msgstr ""
msgid "Save and continue"
msgstr ""
#: src/strings.ts:2942
msgid "Save changes"
msgstr ""
#: src/strings.ts:1066
msgid "Save data recovery key"
msgstr ""

View File

@@ -2938,5 +2938,6 @@ Continue without attachments?`,
t`Something went wrong while preparing your debug logs. This can happen if no storage location was selected.`,
shareZip: () => t`Share ZIP`,
tryDownloadLogsAgain: () => t`Try again`,
createSubnotebook: () => t`Create sub-notebook`
createSubnotebook: () => t`Create sub-notebook`,
saveChanges: () => t`Save changes`
};