From 89645edc73193b3d97bf84cad6efe901af3ad1f6 Mon Sep 17 00:00:00 2001
From: Matthew Yung <117509016+myung03@users.noreply.github.com>
Date: Fri, 24 May 2024 02:29:23 -0700
Subject: [PATCH 1/8] Warp effect (#2501)
* created starfield effect for spacedrop
* fixed starfield hover effects
* changed colours/styling of stars
* eslint fix
---
interface/app/$libraryId/peer/$id.tsx | 14 +-
.../app/$libraryId/peer/StarfieldEffect.tsx | 254 ++++++++++++++++++
2 files changed, 261 insertions(+), 7 deletions(-)
create mode 100644 interface/app/$libraryId/peer/StarfieldEffect.tsx
diff --git a/interface/app/$libraryId/peer/$id.tsx b/interface/app/$libraryId/peer/$id.tsx
index b950f8224..62c76486e 100644
--- a/interface/app/$libraryId/peer/$id.tsx
+++ b/interface/app/$libraryId/peer/$id.tsx
@@ -3,18 +3,14 @@ import { NodeIdParamsSchema } from '~/app/route-schemas';
import { Icon } from '~/components';
import { useRouteTitle, useZodRouteParams } from '~/hooks';
import { hardwareModelToIcon } from '~/util/hardware';
-
import { TopBarPortal } from '../TopBar/Portal';
+import StarfieldEffect from './StarfieldEffect'; // Import the StarfieldEffect component
export const Component = () => {
const { id: _nodeId } = useZodRouteParams(NodeIdParamsSchema);
- // we encode/decode because nodeId has special characters and I'm not willing to change that rn
const nodeId = decodeURIComponent(_nodeId);
-
const peers = usePeers();
-
const peer = peers.get(nodeId);
-
const title = useRouteTitle(peer?.metadata?.name || 'Peer');
return (
@@ -39,8 +35,12 @@ export const Component = () => {
{peer?.metadata.operating_system?.toString()}
{nodeId}
-
- Drop files here to send with Spacedrop
+
+
+
+
+ Drop files here to send with Spacedrop
+
)}
diff --git a/interface/app/$libraryId/peer/StarfieldEffect.tsx b/interface/app/$libraryId/peer/StarfieldEffect.tsx
new file mode 100644
index 000000000..515ee2a21
--- /dev/null
+++ b/interface/app/$libraryId/peer/StarfieldEffect.tsx
@@ -0,0 +1,254 @@
+import React, { useEffect, useRef } from 'react';
+
+const StarfieldEffect: React.FC = () => {
+ const canvasRef = useRef(null);
+
+ useEffect(() => {
+ const canvas = canvasRef.current;
+ if (!canvas) return;
+
+ const ctx = canvas.getContext('2d');
+ if (!ctx) return;
+
+ const resizeCanvas = () => {
+ const scale = window.devicePixelRatio || 1;
+ const width = canvas.parentElement?.clientWidth || 800;
+ const height = canvas.parentElement?.clientHeight || 300;
+ canvas.width = width * scale;
+ canvas.height = height * scale;
+ canvas.style.width = `${width}px`;
+ canvas.style.height = `${height}px`;
+ ctx.scale(scale, scale);
+ };
+
+ resizeCanvas();
+ window.addEventListener('resize', resizeCanvas);
+
+ canvas.style.position = 'absolute';
+ canvas.oncontextmenu = e => e.preventDefault();
+
+ const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
+ const pix = imageData.data;
+
+ const center = { x: canvas.width / 2, y: canvas.height / 2 };
+
+ let mouseActive = false;
+ let mouseDown = false;
+ let mousePos = { x: center.x, y: center.y };
+
+ let starSpeed = 20;
+ const starSpeedMin = starSpeed;
+ const starSpeedMax = 100;
+ const starDistance = 5000;
+
+ let fov = 320;
+ const fovMin = 210;
+ const fovMax = fov;
+
+ const starHolderCount = 8000; // Increased star count
+ const starHolder: any[] = [];
+ const starBgHolder: any[] = [];
+
+ const backgroundColor = { r: 28, g: 29, b: 37, a: 255 };
+
+ const clearImageData = () => {
+ for (let i = 0, l = pix.length; i < l; i += 4) {
+ pix[i] = backgroundColor.r;
+ pix[i + 1] = backgroundColor.g;
+ pix[i + 2] = backgroundColor.b;
+ pix[i + 3] = backgroundColor.a;
+ }
+ };
+
+ const setPixel = (x: number, y: number, r: number, g: number, b: number, a: number) => {
+ const i = (x + y * canvas.width) * 4;
+ pix[i] = r;
+ pix[i + 1] = g;
+ pix[i + 2] = b;
+ pix[i + 3] = a;
+ };
+
+ const setPixelAdditive = (x: number, y: number, r: number, g: number, b: number, a: number) => {
+ const i = (x + y * canvas.width) * 4;
+ pix[i] += r;
+ pix[i + 1] += g;
+ pix[i + 2] += b;
+ pix[i + 3] = a;
+ };
+
+ const drawLine = (x1: number, y1: number, x2: number, y2: number, r: number, g: number, b: number, a: number) => {
+ const dx = Math.abs(x2 - x1);
+ const dy = Math.abs(y2 - y1);
+ const sx = x1 < x2 ? 1 : -1;
+ const sy = y1 < y2 ? 1 : -1;
+ let err = dx - dy;
+ let lx = x1;
+ let ly = y1;
+
+ const continueLoop = true
+ while (continueLoop) {
+ if (lx > 0 && lx < canvas.width && ly > 0 && ly < canvas.height) {
+ setPixel(lx, ly, r, g, b, a);
+ }
+ if (lx === x2 && ly === y2) break;
+ const e2 = 2 * err;
+ if (e2 > -dx) {
+ err -= dy;
+ lx += sx;
+ }
+ if (e2 < dy) {
+ err += dx;
+ ly += sy;
+ }
+ }
+ };
+
+ const addParticle = (x: number, y: number, z: number, ox: number, oy: number, oz: number) => {
+ const particle = { x, y, z, ox, oy, x2d: 0, y2d: 0, color: { r: 0, g: 0, b: 0, a: 0 }, oColor: { r: 0, g: 0, b: 0, a: 0 }, w: 0, distance: 0, distanceTotal: 0 };
+ return particle;
+ };
+
+ const addParticles = () => {
+ let x, y, z, colorValue, particle;
+ for (let i = 0; i < starHolderCount / 3; i++) {
+ x = Math.random() * 24000 - 12000;
+ y = Math.random() * 4500 - 2250;
+ z = Math.round(Math.random() * starDistance);
+ colorValue = 185; // Adjusted color
+ particle = addParticle(x, y, z, x, y, z);
+ particle.color = { r: 171, g: 172, b: 185, a: 255 };
+ starBgHolder.push(particle);
+ }
+ for (let i = 0; i < starHolderCount; i++) {
+ x = Math.random() * 10000 - 5000;
+ y = Math.random() * 10000 - 5000;
+ z = Math.round(Math.random() * starDistance);
+ colorValue = 185; // Adjusted color
+ particle = addParticle(x, y, z, x, y, z);
+ particle.color = { r: 171, g: 172, b: 185, a: 255 };
+ particle.oColor = { r: 171, g: 172, b: 185, a: 255 };
+ particle.w = 1;
+ particle.distance = starDistance - z;
+ particle.distanceTotal = Math.round(starDistance + fov - particle.w);
+ starHolder.push(particle);
+ }
+ };
+
+ const animloop = () => {
+ requestAnimationFrame(animloop);
+ render();
+ };
+
+ const render = () => {
+ clearImageData();
+ let star, scale;
+
+ if (mouseActive) {
+ starSpeed += 2;
+ if (starSpeed > starSpeedMax) starSpeed = starSpeedMax;
+ } else {
+ starSpeed -= 1;
+ if (starSpeed < starSpeedMin) starSpeed = starSpeedMin;
+ }
+
+ fov += mouseActive ? -1 : 0.5;
+ fov = Math.max(fovMin, Math.min(fovMax, fov));
+
+ const warpSpeedValue = starSpeed * (starSpeed / (starSpeedMax / 2));
+
+ for (const bgStar of starBgHolder) {
+ star = bgStar;
+ scale = fov / (fov + star.z);
+ star.x2d = star.x * scale + center.x;
+ star.y2d = star.y * scale + center.y;
+ if (star.x2d > 0 && star.x2d < canvas.width && star.y2d > 0 && star.y2d < canvas.height) {
+ setPixel(star.x2d | 0, star.y2d | 0, star.color.r, star.color.g, star.color.b, 255);
+ }
+ }
+
+ for (const mainStar of starHolder) {
+ star = mainStar;
+ star.z -= starSpeed;
+ star.distance += starSpeed;
+ if (star.z < -fov + star.w) {
+ star.z = starDistance;
+ star.distance = 0;
+ }
+
+ const distancePercent = star.distance / star.distanceTotal;
+ star.color.r = Math.floor(star.oColor.r * distancePercent);
+ star.color.g = Math.floor(star.oColor.g * distancePercent);
+ star.color.b = Math.floor(star.oColor.b * distancePercent);
+
+ scale = fov / (fov + star.z);
+ star.x2d = star.x * scale + center.x;
+ star.y2d = star.y * scale + center.y;
+
+ if (star.x2d > 0 && star.x2d < canvas.width && star.y2d > 0 && star.y2d < canvas.height) {
+ setPixelAdditive(star.x2d | 0, star.y2d | 0, star.color.r, star.color.g, star.color.b, 255);
+ }
+
+ if (starSpeed !== starSpeedMin) {
+ const nz = star.z + warpSpeedValue;
+ scale = fov / (fov + nz);
+ const x2d = star.x * scale + center.x;
+ const y2d = star.y * scale + center.y;
+ if (x2d > 0 && x2d < canvas.width && y2d > 0 && y2d < canvas.height) {
+ drawLine(star.x2d | 0, star.y2d | 0, x2d | 0, y2d | 0, star.color.r, star.color.g, star.color.b, 255);
+ }
+ }
+ }
+
+ ctx.putImageData(imageData, 0, 0);
+
+ center.x += (mousePos.x - center.x) * 0.015;
+ if (!mouseActive) {
+ center.x += (canvas.width / 2 - center.x) * 0.015;
+ }
+ };
+
+ const getMousePos = (event: MouseEvent) => {
+ const rect = canvas.getBoundingClientRect();
+ return { x: event.clientX - rect.left, y: event.clientY - rect.top };
+ };
+
+ const mouseMoveHandler = (event: MouseEvent) => {
+ mousePos = getMousePos(event);
+ };
+
+ const mouseEnterHandler = () => {
+ mouseActive = true;
+ };
+
+ const mouseLeaveHandler = () => {
+ mouseActive = false;
+ mouseDown = false;
+ };
+
+ canvas.addEventListener('mousemove', mouseMoveHandler);
+ canvas.addEventListener('mousedown', () => { mouseDown = true; });
+ canvas.addEventListener('mouseup', () => { mouseDown = false; });
+ canvas.addEventListener('mouseenter', mouseEnterHandler);
+ canvas.addEventListener('mouseleave', mouseLeaveHandler);
+
+ addParticles();
+ animloop();
+
+ return () => {
+ canvas.removeEventListener('mousemove', mouseMoveHandler);
+ canvas.removeEventListener('mousedown', () => { mouseDown = true; });
+ canvas.removeEventListener('mouseup', () => { mouseDown = false; });
+ canvas.removeEventListener('mouseenter', mouseEnterHandler);
+ canvas.removeEventListener('mouseleave', mouseLeaveHandler);
+ window.removeEventListener('resize', resizeCanvas);
+ };
+ }, []);
+
+ return (
+
+ );
+};
+
+export default StarfieldEffect;
From a63d66da4c41ca49c3bac046e1b06ee446f14614 Mon Sep 17 00:00:00 2001
From: Arnab Chakraborty <11457760+Rocky43007@users.noreply.github.com>
Date: Fri, 24 May 2024 14:20:20 -0400
Subject: [PATCH 2/8] No Duplicating Tags (#2505)
* Error when trying to make tags with same name
* Formatting
---
core/src/api/tags.rs | 16 ++++++++++++++++
1 file changed, 16 insertions(+)
diff --git a/core/src/api/tags.rs b/core/src/api/tags.rs
index 73c9c90ca..df813486e 100644
--- a/core/src/api/tags.rs
+++ b/core/src/api/tags.rs
@@ -88,6 +88,22 @@ pub(crate) fn mount() -> AlphaRouter {
.procedure("create", {
R.with2(library())
.mutation(|(_, library), args: TagCreateArgs| async move {
+ // Check if tag with the same name already exists
+ let existing_tag = library
+ .db
+ .tag()
+ .find_many(vec![tag::name::equals(Some(args.name.clone()))])
+ .select(tag::select!({ id }))
+ .exec()
+ .await?;
+
+ if !existing_tag.is_empty() {
+ return Err(rspc::Error::new(
+ ErrorCode::Conflict,
+ "Tag with the same name already exists".to_string(),
+ ));
+ }
+
let created_tag = args.exec(&library).await?;
invalidate_query!(library, "tags.list");
From 2321addc67296e3f6c903d983410ef5ab063a22d Mon Sep 17 00:00:00 2001
From: ameer2468 <33054370+ameer2468@users.noreply.github.com>
Date: Mon, 27 May 2024 16:44:15 +0100
Subject: [PATCH 3/8] [MOB-98] Rename, delete, & more (#2506)
* rename and delete support, browse design improvement
* update toast styling
* Update RenameModal.tsx
* fix test
* fix warning message on initial render, add-tag test, and remove solid js references
* ci
* Add feedback toast for location delete and tag delete
---
apps/mobile/src/App.tsx | 42 +-
.../components/browse/BrowseCategories.tsx | 6 +-
.../src/components/browse/BrowseLocations.tsx | 71 +--
.../src/components/browse/BrowseTags.tsx | 74 +--
.../src/components/drawer/DrawerContent.tsx | 2 +-
.../src/components/drawer/DrawerLocations.tsx | 10 +-
.../src/components/explorer/FileRow.tsx | 2 +-
apps/mobile/src/components/layout/Modal.tsx | 4 +-
.../src/components/locations/GridLocation.tsx | 8 +-
.../src/components/locations/ListLocation.tsx | 6 +-
.../src/components/locations/LocationItem.tsx | 9 +-
.../src/components/modal/AddTagModal.tsx | 4 +-
.../src/components/modal/ImportModal.tsx | 13 +-
.../confirmModals/DeleteLocationModal.tsx | 7 +-
.../modal/confirmModals/DeleteTagModal.tsx | 4 +-
.../modal/inspector/ActionsModal.tsx | 27 +-
.../modal/inspector/RenameModal.tsx | 90 ++++
.../components/modal/tag/CreateTagModal.tsx | 15 +-
.../src/components/overview/Categories.tsx | 6 +-
.../src/components/overview/Locations.tsx | 6 +-
.../src/components/overview/OverviewStats.tsx | 4 +-
.../src/components/primitive/Button.tsx | 4 +-
.../mobile/src/components/primitive/Input.tsx | 18 +-
.../mobile/src/components/primitive/Toast.tsx | 25 +-
apps/mobile/src/components/tags/GridTag.tsx | 2 +-
apps/mobile/src/components/tags/TagItem.tsx | 8 +-
apps/mobile/src/hooks/useFiltersSearch.ts | 2 +-
apps/mobile/src/screens/browse/Location.tsx | 9 +-
apps/mobile/tests/add-tag.yml | 6 +-
pnpm-lock.yaml | 448 +++++-------------
30 files changed, 459 insertions(+), 473 deletions(-)
create mode 100644 apps/mobile/src/components/modal/inspector/RenameModal.tsx
diff --git a/apps/mobile/src/App.tsx b/apps/mobile/src/App.tsx
index ac5f5232b..54e45cb5d 100644
--- a/apps/mobile/src/App.tsx
+++ b/apps/mobile/src/App.tsx
@@ -4,6 +4,19 @@ import {
NavigationContainer,
useNavigationContainerRef
} from '@react-navigation/native';
+import {
+ ClientContextProvider,
+ LibraryContextProvider,
+ P2PContextProvider,
+ RspcProvider,
+ initPlausible,
+ useBridgeQuery,
+ useClientContext,
+ useInvalidateQuery,
+ usePlausibleEvent,
+ usePlausiblePageViewMonitor,
+ usePlausiblePingMonitor
+} from '@sd/client';
import { QueryClient } from '@tanstack/react-query';
import dayjs from 'dayjs';
import advancedFormat from 'dayjs/plugin/advancedFormat';
@@ -17,19 +30,6 @@ import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { MenuProvider } from 'react-native-popup-menu';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { useSnapshot } from 'valtio';
-import {
- ClientContextProvider,
- initPlausible,
- LibraryContextProvider,
- P2PContextProvider,
- RspcProvider,
- useBridgeQuery,
- useClientContext,
- useInvalidateQuery,
- usePlausibleEvent,
- usePlausiblePageViewMonitor,
- usePlausiblePingMonitor
-} from '@sd/client';
import { GlobalModals } from './components/modal/GlobalModals';
import { Toast, toastConfig } from './components/primitive/Toast';
@@ -55,13 +55,17 @@ function AppNavigation() {
const plausibleEvent = usePlausibleEvent();
const buildInfo = useBridgeQuery(['buildInfo']);
- initPlausible({ platformType: 'mobile', buildInfo: buildInfo?.data });
-
const navRef = useNavigationContainerRef();
const routeNameRef = useRef();
const [currentPath, setCurrentPath] = useState('/');
+ useEffect(() => {
+ if (buildInfo?.data) {
+ initPlausible({ platformType: 'mobile', buildInfo: buildInfo.data });
+ }
+ }, [buildInfo]);
+
usePlausiblePageViewMonitor({ currentPath });
usePlausiblePingMonitor({ currentPath });
@@ -73,9 +77,11 @@ function AppNavigation() {
return () => clearInterval(interval);
}, [plausibleEvent]);
- if (library === null && libraries.data) {
- currentLibraryStore.id = libraries.data[0]?.uuid ?? null;
- }
+ useEffect(() => {
+ if (library === null && libraries.data) {
+ currentLibraryStore.id = libraries.data[0]?.uuid ?? null;
+ }
+ }, [library, libraries]);
return (
{
const navigation = useNavigation['navigation']>();
return (
-
+
Library
diff --git a/apps/mobile/src/components/browse/BrowseLocations.tsx b/apps/mobile/src/components/browse/BrowseLocations.tsx
index 819a50e22..991404cab 100644
--- a/apps/mobile/src/components/browse/BrowseLocations.tsx
+++ b/apps/mobile/src/components/browse/BrowseLocations.tsx
@@ -1,14 +1,15 @@
import { useNavigation } from '@react-navigation/native';
import { useLibraryQuery } from '@sd/client';
-import { DotsThree, Plus } from 'phosphor-react-native';
-import { useRef } from 'react';
-import { Text, View } from 'react-native';
+import { useRef, useState } from 'react';
+import { FlatList, Text, View } from 'react-native';
import { ModalRef } from '~/components/layout/Modal';
-import { tw } from '~/lib/tailwind';
+import { tw, twStyle } from '~/lib/tailwind';
import { BrowseStackScreenProps } from '~/navigation/tabs/BrowseStack';
import { SettingsStackScreenProps } from '~/navigation/tabs/SettingsStack';
+import { Plus } from 'phosphor-react-native';
import Empty from '../layout/Empty';
+import Fade from '../layout/Fade';
import { LocationItem } from '../locations/LocationItem';
import ImportModal from '../modal/ImportModal';
import { Button } from '../primitive/Button';
@@ -20,54 +21,64 @@ const BrowseLocations = () => {
>();
const modalRef = useRef(null);
-
+ const [showAll, setShowAll] = useState(false);
const result = useLibraryQuery(['locations.list'], { keepPreviousData: true });
const locations = result.data;
return (
-
-
+
+
Locations
-
- {locations?.length === 0 ? (
-
- ) : (
- <>
- {locations?.slice(0, 3).map((location) => (
+
+
+ }
+ numColumns={showAll ? 3 : 1}
+ horizontal={showAll ? false : true}
+ contentContainerStyle={twStyle(locations?.length === 0 && 'w-full','px-5')}
+ key={showAll ? '_locations' : 'alllocationcols'}
+ keyExtractor={(item) => item.id.toString()}
+ scrollEnabled={showAll ? false : true}
+ showsHorizontalScrollIndicator={false}
+ renderItem={({ item }) => {
+ return (
navigation.navigate('SettingsStack', {
screen: 'EditLocationSettings',
- params: { id: location.id },
+ params: { id: item.id },
initial: false
})
}
- onPress={() => navigation.navigate('Location', { id: location.id })}
- />
- ))}
- >
- )}
+ onPress={() => navigation.navigate('Location', { id: item.id })}
+ />
+ )}
+ }
+ />
+
diff --git a/apps/mobile/src/components/browse/BrowseTags.tsx b/apps/mobile/src/components/browse/BrowseTags.tsx
index 4fc8369ae..a1360362d 100644
--- a/apps/mobile/src/components/browse/BrowseTags.tsx
+++ b/apps/mobile/src/components/browse/BrowseTags.tsx
@@ -1,13 +1,14 @@
import { useNavigation } from '@react-navigation/native';
import { useLibraryQuery } from '@sd/client';
-import { DotsThree, Plus } from 'phosphor-react-native';
-import React, { useRef } from 'react';
-import { Text, View } from 'react-native';
+import { Plus } from 'phosphor-react-native';
+import React, { useRef, useState } from 'react';
+import { FlatList, Text, View } from 'react-native';
import { ModalRef } from '~/components/layout/Modal';
-import { tw } from '~/lib/tailwind';
+import { tw, twStyle } from '~/lib/tailwind';
import { BrowseStackScreenProps } from '~/navigation/tabs/BrowseStack';
import Empty from '../layout/Empty';
+import Fade from '../layout/Fade';
import CreateTagModal from '../modal/tag/CreateTagModal';
import { Button } from '../primitive/Button';
import { TagItem } from '../tags/TagItem';
@@ -19,47 +20,58 @@ const BrowseTags = () => {
const tagData = tags.data;
const modalRef = useRef(null);
+ const [showAll, setShowAll] = useState(false);
return (
-
-
+
+
Tags
-
- {tagData?.length === 0 ? (
-
- ) : (
- tagData
- ?.slice(0, 3)
- .map((tag) => (
+
+
+ }
+ numColumns={showAll ? 3 : 1}
+ contentContainerStyle={twStyle(tagData?.length === 0 && 'w-full','px-5')}
+ horizontal={showAll ? false : true}
+ key={showAll ? '_tags' : 'alltagcols'}
+ keyExtractor={(item) => item.id.toString()}
+ scrollEnabled={showAll ? false : true}
+ showsHorizontalScrollIndicator={false}
+ renderItem={({ item }) => (
- navigation.navigate('Tag', { id: tag.id, color: tag.color! })
- }
- />
- ))
- )}
+ style={twStyle(showAll && 'max-w-[31%] flex-1')}
+ key={item.id}
+ tag={item}
+ onPress={() =>
+ navigation.navigate('Tag', { id: item.id, color: item.color! })
+ }
+ />
+ )}
+ />
+
diff --git a/apps/mobile/src/components/drawer/DrawerContent.tsx b/apps/mobile/src/components/drawer/DrawerContent.tsx
index 02817e080..b46099351 100644
--- a/apps/mobile/src/components/drawer/DrawerContent.tsx
+++ b/apps/mobile/src/components/drawer/DrawerContent.tsx
@@ -1,11 +1,11 @@
import { DrawerContentScrollView } from '@react-navigation/drawer';
import { DrawerContentComponentProps } from '@react-navigation/drawer/lib/typescript/src/types';
import { AppLogo } from '@sd/assets/images';
+import { JobManagerContextProvider, useLibraryQuery } from '@sd/client';
import { Image } from 'expo-image';
import { CheckCircle } from 'phosphor-react-native';
import { useRef } from 'react';
import { Platform, Pressable, Text, View } from 'react-native';
-import { JobManagerContextProvider, useLibraryQuery } from '@sd/client';
import Layout from '~/constants/Layout';
import { tw, twStyle } from '~/lib/tailwind';
diff --git a/apps/mobile/src/components/drawer/DrawerLocations.tsx b/apps/mobile/src/components/drawer/DrawerLocations.tsx
index 28a367cb5..2af0da0f6 100644
--- a/apps/mobile/src/components/drawer/DrawerLocations.tsx
+++ b/apps/mobile/src/components/drawer/DrawerLocations.tsx
@@ -1,14 +1,14 @@
import { DrawerNavigationHelpers } from '@react-navigation/drawer/lib/typescript/src/types';
import { useNavigation } from '@react-navigation/native';
-import { useRef } from 'react';
-import { Pressable, Text, View } from 'react-native';
import {
+ Location,
arraysEqual,
humanizeSize,
- Location,
useLibraryQuery,
useOnlineLocations
} from '@sd/client';
+import { useRef } from 'react';
+import { Pressable, Text, View } from 'react-native';
import { ModalRef } from '~/components/layout/Modal';
import { tw, twStyle } from '~/lib/tailwind';
@@ -49,8 +49,8 @@ const DrawerLocationItem: React.FC = ({
{location.name ?? ''}
-
-
+
+
{`${humanizeSize(location.size_in_bytes)}`}
diff --git a/apps/mobile/src/components/explorer/FileRow.tsx b/apps/mobile/src/components/explorer/FileRow.tsx
index d376c0177..25ac79a5f 100644
--- a/apps/mobile/src/components/explorer/FileRow.tsx
+++ b/apps/mobile/src/components/explorer/FileRow.tsx
@@ -27,7 +27,7 @@ const FileRow = ({ data }: FileRowProps) => {
height: getExplorerStore().listItemSize
})}
>
-
+
diff --git a/apps/mobile/src/components/layout/Modal.tsx b/apps/mobile/src/components/layout/Modal.tsx
index 6d2b60524..ecdc86872 100644
--- a/apps/mobile/src/components/layout/Modal.tsx
+++ b/apps/mobile/src/components/layout/Modal.tsx
@@ -6,10 +6,10 @@ import {
BottomSheetHandleProps,
BottomSheetModal,
BottomSheetModalProps,
- BottomSheetScrollView
+ BottomSheetScrollView,
} from '@gorhom/bottom-sheet';
import { X } from 'phosphor-react-native';
-import { forwardRef, ReactNode } from 'react';
+import { ReactNode, forwardRef } from 'react';
import { Platform, Pressable, Text, View } from 'react-native';
import useForwardedRef from '~/hooks/useForwardedRef';
import { tw, twStyle } from '~/lib/tailwind';
diff --git a/apps/mobile/src/components/locations/GridLocation.tsx b/apps/mobile/src/components/locations/GridLocation.tsx
index ed40b9c25..47c8e1853 100644
--- a/apps/mobile/src/components/locations/GridLocation.tsx
+++ b/apps/mobile/src/components/locations/GridLocation.tsx
@@ -1,6 +1,6 @@
+import { Location, arraysEqual, humanizeSize, useOnlineLocations } from '@sd/client';
import { DotsThreeOutlineVertical } from 'phosphor-react-native';
import { Pressable, Text, View } from 'react-native';
-import { arraysEqual, humanizeSize, Location, useOnlineLocations } from '@sd/client';
import { tw, twStyle } from '~/lib/tailwind';
import FolderIcon from '../icons/FolderIcon';
@@ -16,7 +16,7 @@ const GridLocation: React.FC = ({ location, modalRef }: GridL
const onlineLocations = useOnlineLocations();
const online = onlineLocations.some((l) => arraysEqual(location.pub_id, l));
return (
-
+
@@ -46,9 +46,11 @@ const GridLocation: React.FC = ({ location, modalRef }: GridL
{location.path}
-
+
+
{`${humanizeSize(location.size_in_bytes)}`}
+
);
};
diff --git a/apps/mobile/src/components/locations/ListLocation.tsx b/apps/mobile/src/components/locations/ListLocation.tsx
index 90fc66b8b..66b242a18 100644
--- a/apps/mobile/src/components/locations/ListLocation.tsx
+++ b/apps/mobile/src/components/locations/ListLocation.tsx
@@ -61,10 +61,10 @@ const ListLocation = ({ location }: ListLocationProps) => {
-
-
+
+
{`${humanizeSize(location.size_in_bytes)}`}
diff --git a/apps/mobile/src/components/locations/LocationItem.tsx b/apps/mobile/src/components/locations/LocationItem.tsx
index fae5729db..a9f476724 100644
--- a/apps/mobile/src/components/locations/LocationItem.tsx
+++ b/apps/mobile/src/components/locations/LocationItem.tsx
@@ -1,8 +1,9 @@
+import { Location } from '@sd/client';
import { useRef } from 'react';
import { Pressable } from 'react-native';
-import { Location } from '@sd/client';
import { twStyle } from '~/lib/tailwind';
+import { ClassInput } from 'twrnc';
import { ModalRef } from '../layout/Modal';
import { LocationModal } from '../modal/location/LocationModal';
import GridLocation from './GridLocation';
@@ -13,19 +14,21 @@ type LocationItemProps = {
onPress: () => void;
viewStyle?: 'grid' | 'list';
editLocation: () => void;
+ style?: ClassInput;
};
export const LocationItem = ({
location,
onPress,
editLocation,
- viewStyle = 'grid'
+ viewStyle = 'grid',
+ style
}: LocationItemProps) => {
const modalRef = useRef(null);
return (
<>
{viewStyle === 'grid' ? (
diff --git a/apps/mobile/src/components/modal/AddTagModal.tsx b/apps/mobile/src/components/modal/AddTagModal.tsx
index 1beee726a..e45d16502 100644
--- a/apps/mobile/src/components/modal/AddTagModal.tsx
+++ b/apps/mobile/src/components/modal/AddTagModal.tsx
@@ -1,4 +1,4 @@
-import { Tag, getItemObject, useLibraryMutation, useLibraryQuery, useRspcContext } from "@sd/client";
+import { Tag, getItemObject, useLibraryMutation, useLibraryQuery, useRspcLibraryContext } from "@sd/client";
import { CaretLeft, Plus } from "phosphor-react-native";
import { forwardRef, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { FlatList, NativeScrollEvent, Pressable, Text, View } from "react-native";
@@ -24,7 +24,7 @@ const AddTagModal = forwardRef((_, ref) => {
const [startedScrolling, setStartedScrolling] = useState(false);
const [reachedBottom, setReachedBottom] = useState(true); // needs to be set to true for initial rendering fade to be correct
- const rspc = useRspcContext();
+ const rspc = useRspcLibraryContext();
const tagsQuery = useLibraryQuery(['tags.list']);
const tagsObjectQuery = useLibraryQuery(['tags.getForObject', objectData?.id ?? -1]);
const mutation = useLibraryMutation(['tags.assign'], {
diff --git a/apps/mobile/src/components/modal/ImportModal.tsx b/apps/mobile/src/components/modal/ImportModal.tsx
index 70e98a93f..7980284c9 100644
--- a/apps/mobile/src/components/modal/ImportModal.tsx
+++ b/apps/mobile/src/components/modal/ImportModal.tsx
@@ -1,14 +1,15 @@
import * as RNFS from '@dr.pogodin/react-native-fs';
+import { useLibraryMutation, useRspcLibraryContext } from '@sd/client';
import { forwardRef, useCallback } from 'react';
import { Alert, Platform, Text, View } from 'react-native';
import DocumentPicker from 'react-native-document-picker';
-import { useLibraryMutation, useRspcLibraryContext } from '@sd/client';
import { Modal, ModalRef } from '~/components/layout/Modal';
import { Button } from '~/components/primitive/Button';
import useForwardedRef from '~/hooks/useForwardedRef';
import { tw } from '~/lib/tailwind';
import { Icon } from '../icons/Icon';
+import { toast } from '../primitive/Toast';
// import * as ML from 'expo-media-library';
@@ -22,17 +23,27 @@ const ImportModal = forwardRef((_, ref) => {
const createLocation = useLibraryMutation('locations.create', {
onError: (error, variables) => {
+ modalRef.current?.close();
+ //custom message handling
+ if (error.message.startsWith("location already exists")) {
+ return toast.error('This location has already been added');
+ }
switch (error.message) {
case 'NEED_RELINK':
if (!variables.dry_run) relinkLocation.mutate(variables.path);
+ toast.info('Please relink the location');
break;
case 'ADD_LIBRARY':
addLocationToLibrary.mutate(variables);
break;
default:
+ toast.error(error.message);
throw new Error('Unimplemented custom remote error handling');
}
},
+ onSuccess: () => {
+ toast.success('Location added successfully');
+ },
onSettled: () => {
rspc.queryClient.invalidateQueries(['locations.list']);
modalRef.current?.close();
diff --git a/apps/mobile/src/components/modal/confirmModals/DeleteLocationModal.tsx b/apps/mobile/src/components/modal/confirmModals/DeleteLocationModal.tsx
index 9dc6a59fd..8d221305e 100644
--- a/apps/mobile/src/components/modal/confirmModals/DeleteLocationModal.tsx
+++ b/apps/mobile/src/components/modal/confirmModals/DeleteLocationModal.tsx
@@ -1,6 +1,7 @@
-import { useRef } from 'react';
import { useLibraryMutation, usePlausibleEvent, useRspcLibraryContext } from '@sd/client';
+import { useRef } from 'react';
import { ConfirmModal, ModalRef } from '~/components/layout/Modal';
+import { toast } from '~/components/primitive/Toast';
type Props = {
locationId: number;
@@ -20,6 +21,10 @@ const DeleteLocationModal = ({ trigger, onSubmit, locationId, triggerStyle }: Pr
onSuccess: () => {
submitPlausibleEvent({ event: { type: 'locationDelete' } });
onSubmit?.();
+ toast.success('Location deleted successfully');
+ },
+ onError: (error) => {
+ toast.error(error.message);
},
onSettled: () => {
modalRef.current?.close();
diff --git a/apps/mobile/src/components/modal/confirmModals/DeleteTagModal.tsx b/apps/mobile/src/components/modal/confirmModals/DeleteTagModal.tsx
index 54eb5e8cf..8d68272cc 100644
--- a/apps/mobile/src/components/modal/confirmModals/DeleteTagModal.tsx
+++ b/apps/mobile/src/components/modal/confirmModals/DeleteTagModal.tsx
@@ -1,6 +1,7 @@
-import { useRef } from 'react';
import { useLibraryMutation, usePlausibleEvent, useRspcLibraryContext } from '@sd/client';
+import { useRef } from 'react';
import { ConfirmModal, ModalRef } from '~/components/layout/Modal';
+import { toast } from '~/components/primitive/Toast';
type Props = {
tagId: number;
@@ -19,6 +20,7 @@ const DeleteTagModal = ({ trigger, onSubmit, tagId, triggerStyle }: Props) => {
submitPlausibleEvent({ event: { type: 'tagDelete' } });
onSubmit?.();
rspc.queryClient.invalidateQueries(['tags.list']);
+ toast.success('Tag deleted successfully');
},
onSettled: () => {
modalRef.current?.close();
diff --git a/apps/mobile/src/components/modal/inspector/ActionsModal.tsx b/apps/mobile/src/components/modal/inspector/ActionsModal.tsx
index b0dcd79ee..f310ad2c9 100644
--- a/apps/mobile/src/components/modal/inspector/ActionsModal.tsx
+++ b/apps/mobile/src/components/modal/inspector/ActionsModal.tsx
@@ -3,7 +3,8 @@ import {
getItemObject,
humanizeSize,
useLibraryMutation,
- useLibraryQuery
+ useLibraryQuery,
+ useRspcContext
} from '@sd/client';
import dayjs from 'dayjs';
import {
@@ -28,6 +29,7 @@ import { tw, twStyle } from '~/lib/tailwind';
import { useActionsModalStore } from '~/stores/modalStore';
import FileInfoModal from './FileInfoModal';
+import RenameModal from './RenameModal';
type ActionsContainerProps = PropsWithChildren<{
style?: ViewStyle;
@@ -65,8 +67,10 @@ const ActionDivider = () => ;
export const ActionsModal = () => {
const fileInfoRef = useRef(null);
+ const renameRef = useRef(null);
const { modalRef, data } = useActionsModalStore();
+ const rspc = useRspcContext();
const objectData = data && getItemObject(data);
const filePath = data && getIndexedItemFilePath(data);
@@ -77,6 +81,13 @@ export const ActionsModal = () => {
enabled: filePath != null
});
+ const deleteFile = useLibraryMutation('files.deleteFiles', {
+ onSuccess: () => {
+ rspc.queryClient.invalidateQueries(['search.paths'])
+ modalRef.current?.dismiss();
+ }
+ });
+
async function handleOpen() {
const absolutePath = queriedFullPath.data;
if (!absolutePath) return;
@@ -141,7 +152,9 @@ export const ActionsModal = () => {
/>
-
+ {
+ renameRef.current?.present();
+ }} icon={Pencil} title="Rename" />
@@ -154,11 +167,19 @@ export const ActionsModal = () => {
-
+ {
+ if (filePath && filePath.location_id) {
+ await deleteFile.mutateAsync({
+ location_id: filePath.location_id,
+ file_path_ids: [filePath.id]
+ });
+ }
+ }} />
)}
+
>
);
diff --git a/apps/mobile/src/components/modal/inspector/RenameModal.tsx b/apps/mobile/src/components/modal/inspector/RenameModal.tsx
new file mode 100644
index 000000000..4bc7f100b
--- /dev/null
+++ b/apps/mobile/src/components/modal/inspector/RenameModal.tsx
@@ -0,0 +1,90 @@
+import { getIndexedItemFilePath, useLibraryMutation, useRspcLibraryContext } from '@sd/client';
+import React, { forwardRef, useEffect, useRef, useState } from 'react';
+import { Text, View } from 'react-native';
+import { TextInput } from 'react-native-gesture-handler';
+import { Modal, ModalRef } from '~/components/layout/Modal';
+import { Button } from '~/components/primitive/Button';
+import { ModalInput } from '~/components/primitive/Input';
+import { toast } from '~/components/primitive/Toast';
+import useForwardedRef from '~/hooks/useForwardedRef';
+import { tw } from '~/lib/tailwind';
+import { useActionsModalStore } from '~/stores/modalStore';
+
+interface Props {
+ objectName: string;
+}
+
+const RenameModal = forwardRef((props, ref) => {
+ const modalRef = useForwardedRef(ref);
+ const [newName, setNewName] = useState('');
+ const rspc = useRspcLibraryContext();
+ const { data } = useActionsModalStore();
+ const inputRef = useRef(null);
+
+ const filePathData = data && getIndexedItemFilePath(data);
+
+ const renameFile = useLibraryMutation(['files.renameFile'], {
+ onSuccess: () => {
+ modalRef.current?.dismiss();
+ rspc.queryClient.invalidateQueries(['search.paths']);
+ },
+ onError: () => {
+ toast.error('Failed to rename object');
+ }
+ });
+
+ // set input value to object name on initial render
+ useEffect(() => {
+ setNewName(props.objectName);
+ }, [props.objectName]);
+
+ const textRenameHandler = async () => {
+ switch (data?.type) {
+ case 'Path':
+ case 'Object': {
+ if (!filePathData) throw new Error('Failed to get file path object');
+
+ const { id, location_id } = filePathData;
+
+ if (!location_id) throw new Error('Missing location id');
+
+ await renameFile.mutateAsync({
+ location_id: location_id,
+ kind: {
+ One: {
+ from_file_path_id: id,
+ to: newName
+ }
+ }
+ });
+ break;
+ }
+ }
+ };
+
+ return (
+ setNewName(props.objectName)}
+ enableContentPanningGesture={false}
+ enablePanDownToClose={false}
+ snapPoints={['20']}
+ >
+
+ inputRef.current?.setSelection(0, newName.length)}
+ value={newName}
+ onChangeText={(t) => setNewName(t)}
+ />
+
+
+
+ );
+});
+
+export default RenameModal;
diff --git a/apps/mobile/src/components/modal/tag/CreateTagModal.tsx b/apps/mobile/src/components/modal/tag/CreateTagModal.tsx
index d92fc40cf..ff4f9fdd0 100644
--- a/apps/mobile/src/components/modal/tag/CreateTagModal.tsx
+++ b/apps/mobile/src/components/modal/tag/CreateTagModal.tsx
@@ -1,16 +1,17 @@
-import { forwardRef, useEffect, useState } from 'react';
-import { Pressable, Text, View } from 'react-native';
-import ColorPicker from 'react-native-wheel-color-picker';
import {
ToastDefautlColor,
useLibraryMutation,
usePlausibleEvent,
useRspcLibraryContext
} from '@sd/client';
+import { forwardRef, useEffect, useState } from 'react';
+import { Pressable, Text, View } from 'react-native';
+import ColorPicker from 'react-native-wheel-color-picker';
import { FadeInAnimation } from '~/components/animation/layout';
import { Modal, ModalRef } from '~/components/layout/Modal';
import { Button } from '~/components/primitive/Button';
import { ModalInput } from '~/components/primitive/Input';
+import { toast } from '~/components/primitive/Toast';
import useForwardedRef from '~/hooks/useForwardedRef';
import { useKeyboard } from '~/hooks/useKeyboard';
import { tw, twStyle } from '~/lib/tailwind';
@@ -36,8 +37,12 @@ const CreateTagModal = forwardRef((_, ref) => {
rspc.queryClient.invalidateQueries(['tags.list']);
+ toast.success('Tag created successfully');
submitPlausibleEvent({ event: { type: 'tagCreate' } });
},
+ onError: (error) => {
+ toast.error(error.message);
+ },
onSettled: () => {
// Close modal
modalRef.current?.dismiss();
@@ -57,7 +62,7 @@ const CreateTagModal = forwardRef((_, ref) => {
return (
{
// Resets form onDismiss
@@ -94,7 +99,7 @@ const CreateTagModal = forwardRef((_, ref) => {