Networking
diff --git a/interface/app/$libraryId/settings/library/tags/CreateDialog.tsx b/interface/app/$libraryId/settings/library/tags/CreateDialog.tsx
index 46bd14181..dfd89111b 100644
--- a/interface/app/$libraryId/settings/library/tags/CreateDialog.tsx
+++ b/interface/app/$libraryId/settings/library/tags/CreateDialog.tsx
@@ -2,6 +2,7 @@ import {
FilePath,
Object,
Target,
+ ToastDefautlColor,
useLibraryMutation,
usePlausibleEvent,
useZodForm
@@ -53,7 +54,7 @@ export default (
const form = useZodForm({
schema: schema,
- defaultValues: { color: '#A717D9' }
+ defaultValues: { color: ToastDefautlColor }
});
const createTag = useLibraryMutation('tags.create');
diff --git a/interface/index.tsx b/interface/index.tsx
index b832df5d4..60525fea4 100644
--- a/interface/index.tsx
+++ b/interface/index.tsx
@@ -8,13 +8,12 @@ import relativeTime from 'dayjs/plugin/relativeTime';
import { PropsWithChildren, Suspense } from 'react';
import { RouterProvider, RouterProviderProps } from 'react-router-dom';
import {
- CacheProvider,
- NotificationContextProvider,
P2PContextProvider,
+ useBridgeSubscription,
useInvalidateQuery,
useLoadBackendFeatureFlags
} from '@sd/client';
-import { TooltipProvider } from '@sd/ui';
+import { toast, TooltipProvider } from '@sd/ui';
import { createRoutes } from './app';
import { P2P, useP2PErrorToast } from './app/p2p';
@@ -24,11 +23,11 @@ import ErrorFallback, { BetterErrorBoundary } from './ErrorFallback';
import { useTheme } from './hooks';
import { RoutingContext } from './RoutingContext';
-export { ErrorPage } from './ErrorFallback';
export * from './app';
-export * from './util/Platform';
-export * from './util/keybind';
+export { ErrorPage } from './ErrorFallback';
export * from './TabsContext';
+export * from './util/keybind';
+export * from './util/Platform';
dayjs.extend(advancedFormat);
dayjs.extend(relativeTime);
@@ -77,17 +76,22 @@ export function SpacedriveInterfaceRoot({ children }: PropsWithChildren) {
useInvalidateQuery();
useTheme();
+ useBridgeSubscription(['notifications.listen'], {
+ onData({ data: { title, content, kind }, expires }) {
+ console.log(expires);
+ toast({ title, body: content }, { type: kind });
+ }
+ });
+
return (
-
-
-
-
- {children}
-
+
+
+
+ {children}
diff --git a/packages/client/src/color.ts b/packages/client/src/color.ts
new file mode 100644
index 000000000..dc7e6cdb6
--- /dev/null
+++ b/packages/client/src/color.ts
@@ -0,0 +1 @@
+export const ToastDefautlColor = '#A717D9';
diff --git a/packages/client/src/core.ts b/packages/client/src/core.ts
index 664928f51..9b8225a66 100644
--- a/packages/client/src/core.ts
+++ b/packages/client/src/core.ts
@@ -16,6 +16,10 @@ export type Procedures = {
{ key: "invalidation.test-invalidate", input: never, result: number } |
{ key: "jobs.isActive", input: LibraryArgs, result: boolean } |
{ key: "jobs.reports", input: LibraryArgs, result: JobGroup[] } |
+ { key: "labels.get", input: LibraryArgs, result: { id: number; pub_id: number[]; name: string; date_created: string; date_modified: string } | null } |
+ { key: "labels.getForObject", input: LibraryArgs, result: Label[] } |
+ { key: "labels.getWithObjects", input: LibraryArgs, result: { [key in number]: { date_created: string; object: { id: number } }[] } } |
+ { key: "labels.list", input: LibraryArgs, result: Label[] } |
{ key: "library.list", input: never, result: NormalisedResults } |
{ key: "library.statistics", input: LibraryArgs, result: Statistics } |
{ key: "locations.get", input: LibraryArgs, result: { item: Reference; nodes: CacheNode[] } | null } |
@@ -25,6 +29,7 @@ export type Procedures = {
{ key: "locations.indexer_rules.listForLocation", input: LibraryArgs, result: NormalisedResults } |
{ key: "locations.list", input: LibraryArgs, result: NormalisedResults } |
{ key: "locations.systemLocations", input: never, result: SystemLocations } |
+ { key: "models.image_detection.list", input: never, result: string[] } |
{ key: "nodeState", input: never, result: NodeState } |
{ key: "nodes.listLocations", input: LibraryArgs, result: ExplorerItem[] } |
{ key: "notifications.dismiss", input: NotificationId, result: null } |
@@ -72,11 +77,13 @@ export type Procedures = {
{ key: "jobs.cancel", input: LibraryArgs, result: null } |
{ key: "jobs.clear", input: LibraryArgs, result: null } |
{ key: "jobs.clearAll", input: LibraryArgs, result: null } |
+ { key: "jobs.generateLabelsForLocation", input: LibraryArgs, result: null } |
{ key: "jobs.generateThumbsForLocation", input: LibraryArgs, result: null } |
{ key: "jobs.identifyUniqueFiles", input: LibraryArgs, result: null } |
{ key: "jobs.objectValidator", input: LibraryArgs, result: null } |
{ key: "jobs.pause", input: LibraryArgs, result: null } |
{ key: "jobs.resume", input: LibraryArgs, result: null } |
+ { key: "labels.delete", input: LibraryArgs, result: null } |
{ key: "library.create", input: CreateLibraryArgs, result: NormalisedResult } |
{ key: "library.delete", input: string, result: null } |
{ key: "library.edit", input: EditLibraryArgs, result: null } |
@@ -93,8 +100,6 @@ export type Procedures = {
{ key: "locations.update", input: LibraryArgs, result: null } |
{ key: "nodes.edit", input: ChangeNodeNameArgs, result: null } |
{ key: "nodes.updateThumbnailerPreferences", input: UpdateThumbnailerPreferences, result: null } |
- { key: "notifications.test", input: never, result: null } |
- { key: "notifications.testLibrary", input: LibraryArgs, result: null } |
{ key: "p2p.acceptSpacedrop", input: [string, string | null], result: null } |
{ key: "p2p.cancelSpacedrop", input: string, result: null } |
{ key: "p2p.pair", input: RemoteIdentity, result: number } |
@@ -145,7 +150,7 @@ export type CacheNode = { __type: string; __id: string; "#node": any }
export type CameraData = { device_make: string | null; device_model: string | null; color_space: string | null; color_profile: ColorProfile | null; focal_length: number | null; shutter_speed: number | null; flash: Flash | null; orientation: Orientation; lens_make: string | null; lens_model: string | null; bit_depth: number | null; red_eye: boolean | null; zoom: number | null; iso: number | null; software: string | null; serial_number: string | null; lens_serial_number: string | null; contrast: number | null; saturation: number | null; sharpness: number | null; composite: Composite | null }
-export type ChangeNodeNameArgs = { name: string | null; p2p_enabled: boolean | null; p2p_port: MaybeUndefined }
+export type ChangeNodeNameArgs = { name: string | null; p2p_port: MaybeUndefined; p2p_enabled: boolean | null; image_labeler_version: string | null }
export type CloudInstance = { id: string; uuid: string; identity: string }
@@ -295,6 +300,8 @@ export type FromPattern = { pattern: string; replace_all: boolean }
export type FullRescanArgs = { location_id: number; reidentify_objects: boolean }
+export type GenerateLabelsForLocationArgs = { id: number; path: string; regenerate?: boolean }
+
export type GenerateThumbsForLocationArgs = { id: number; path: string; regenerate?: boolean }
export type GetAll = { backups: Backup[]; directory: string }
@@ -331,6 +338,8 @@ export type JobStatus = "Queued" | "Running" | "Completed" | "Canceled" | "Faile
export type JsonValue = null | boolean | number | string | JsonValue[] | { [key in string]: JsonValue }
+export type Label = { id: number; pub_id: number[]; name: string; date_created: string; date_modified: string }
+
/**
* Can wrap a query argument to require it to contain a `library_id` and provide helpers for working with libraries.
*/
@@ -412,7 +421,7 @@ id: string;
/**
* name is the display name of the current node. This is set by the user and is shown in the UI. // TODO: Length validation so it can fit in DNS record
*/
-name: string; p2p_enabled: boolean; p2p_port: number | null; features: BackendFeature[]; preferences: NodePreferences }) & { data_path: string; p2p: P2PStatus }
+name: string; p2p_enabled: boolean; p2p_port: number | null; features: BackendFeature[]; preferences: NodePreferences; image_labeler_version: string | null }) & { data_path: string; p2p: P2PStatus }
export type NonIndexedPathItem = { path: string; name: string; extension: string; kind: number; is_dir: boolean; date_created: string; date_modified: string; size_in_bytes_bytes: number[]; hidden: boolean }
@@ -439,10 +448,12 @@ export type Notification = ({ type: "library"; id: [string, number] } | { type:
* Represents the data of a single notification.
* This data is used by the frontend to properly display the notification.
*/
-export type NotificationData = { PairingRequest: { id: string; pairing_id: number } } | "Test"
+export type NotificationData = { title: string; content: string; kind: NotificationKind }
export type NotificationId = { type: "library"; id: [string, number] } | { type: "node"; id: number }
+export type NotificationKind = "info" | "success" | "error" | "warning"
+
export type Object = { id: number; pub_id: number[]; kind: number | null; key_id: number | null; hidden: boolean | null; favorite: boolean | null; important: boolean | null; note: string | null; date_created: string | null; date_accessed: string | null }
export type ObjectCursor = "none" | { dateAccessed: CursorOrderItem } | { kind: CursorOrderItem }
@@ -548,7 +559,7 @@ export type Statistics = { id: number; date_captured: string; total_object_count
export type SystemLocations = { desktop: string | null; documents: string | null; downloads: string | null; pictures: string | null; music: string | null; videos: string | null }
-export type Tag = { id: number; pub_id: number[]; name: string | null; color: string | null; redundancy_goal: number | null; date_created: string | null; date_modified: string | null }
+export type Tag = { id: number; pub_id: number[]; name: string | null; color: string | null; is_hidden: boolean | null; date_created: string | null; date_modified: string | null }
export type TagCreateArgs = { name: string; color: string }
diff --git a/packages/client/src/hooks/index.ts b/packages/client/src/hooks/index.ts
index 9904b271e..6bc293f70 100644
--- a/packages/client/src/hooks/index.ts
+++ b/packages/client/src/hooks/index.ts
@@ -1,6 +1,8 @@
export * from './useClientContext';
export * from './useDebugState';
+export * from './useExplorerLayoutStore';
export * from './useFeatureFlag';
+export * from './useForceUpdate';
export * from './useLibraryContext';
export * from './useLibraryStore';
export * from './useOnboardingStore';
@@ -8,7 +10,4 @@ export * from './useP2PEvents';
export * from './usePlausible';
export * from './useTelemetryState';
export * from './useThemeStore';
-export * from './useNotifications';
-export * from './useForceUpdate';
export * from './useUnitFormatStore';
-export * from './useExplorerLayoutStore';
diff --git a/packages/client/src/hooks/useNotifications.tsx b/packages/client/src/hooks/useNotifications.tsx
deleted file mode 100644
index 44ce1fae8..000000000
--- a/packages/client/src/hooks/useNotifications.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-import { createContext, PropsWithChildren, useState } from 'react';
-
-import { Notification } from '../core';
-import { useBridgeSubscription } from '../rspc';
-
-type Context = {
- notifications: Set;
-};
-
-const Context = createContext(null as any);
-
-export function NotificationContextProvider({ children }: PropsWithChildren) {
- const [[notifications], setNotifications] = useState([new Set()]);
-
- useBridgeSubscription(['notifications.listen'], {
- onData(data) {
- setNotifications([notifications.add(data)]);
- }
- });
-
- return (
-
- {children}
-
- );
-}
diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts
index 888d3e8cc..e1fb523cf 100644
--- a/packages/client/src/index.ts
+++ b/packages/client/src/index.ts
@@ -28,3 +28,4 @@ export * from './utils';
export * from './lib';
export * from './form';
export * from './cache';
+export * from './color';
diff --git a/packages/client/src/utils/jobs/useJobInfo.tsx b/packages/client/src/utils/jobs/useJobInfo.tsx
index c0e9fc9ff..1ab0c8181 100644
--- a/packages/client/src/utils/jobs/useJobInfo.tsx
+++ b/packages/client/src/utils/jobs/useJobInfo.tsx
@@ -93,6 +93,18 @@ export function useJobInfo(job: JobReport, realtimeUpdate: JobProgressEvent | nu
];
}
+ case 'labels': {
+ return [
+ {
+ text: `Labeled ${
+ completedTaskCount
+ ? formatNumber(completedTaskCount || 0)
+ : formatNumber(output?.labels_extracted)
+ } of ${formatNumber(taskCount)} ${plural(taskCount, 'file')}`
+ }
+ ];
+ }
+
default: {
// If we don't have a phase set, then we're done
diff --git a/scripts/tauri.mjs b/scripts/tauri.mjs
index fba0032a7..87c3d45b4 100755
--- a/scripts/tauri.mjs
+++ b/scripts/tauri.mjs
@@ -10,6 +10,7 @@ import * as toml from '@iarna/toml'
import { waitLockUnlock } from './utils/flock.mjs'
import { patchTauri } from './utils/patchTauri.mjs'
+import { symlinkSharedLibsLinux } from './utils/shared.mjs'
import spawn from './utils/spawn.mjs'
if (/^(msys|mingw|cygwin)$/i.test(env.OSTYPE ?? '')) {
@@ -74,14 +75,18 @@ const bundles = args
.flatMap(target => target.split(','))
let code = 0
+
+if (process.platform === 'linux' && (args[0] === 'dev' || args[0] === 'build'))
+ await symlinkSharedLibsLinux(__root, nativeDeps)
+
try {
switch (args[0]) {
case 'dev': {
__cleanup.push(...(await patchTauri(__root, nativeDeps, targets, bundles, args)))
switch (process.platform) {
- case 'darwin':
case 'linux':
+ case 'darwin':
void waitLockUnlock(path.join(__root, 'target', 'debug', '.cargo-lock')).then(
() => setTimeout(1000).then(cleanUp),
() => {}
diff --git a/scripts/utils/patchTauri.mjs b/scripts/utils/patchTauri.mjs
index fbc494ebc..d8b1d033b 100644
--- a/scripts/utils/patchTauri.mjs
+++ b/scripts/utils/patchTauri.mjs
@@ -7,7 +7,7 @@ import { promisify } from 'node:util'
import * as semver from 'semver'
-import { copyLinuxLibs, copyWindowsDLLs } from './shared.mjs'
+import { linuxLibs, windowsDLLs } from './shared.mjs'
const exec = promisify(_exec)
const __debug = env.NODE_ENV === 'debug'
@@ -62,28 +62,28 @@ export async function patchTauri(root, nativeDeps, targets, bundles, args) {
throw new Error('Custom tauri build config is not supported.')
}
- // Location for desktop app tauri code
- const tauriRoot = path.join(root, 'apps', 'desktop', 'src-tauri')
-
const osType = os.type()
- const resources =
- osType === 'Linux'
- ? await copyLinuxLibs(root, nativeDeps, args[0] === 'dev')
- : osType === 'Windows_NT'
- ? await copyWindowsDLLs(root, nativeDeps)
- : { files: [], toClean: [] }
const tauriPatch = {
tauri: {
bundle: {
- macOS: {
- minimumSystemVersion: '',
- },
- resources: resources.files,
+ macOS: { minimumSystemVersion: '' },
+ resources: {},
},
updater: /** @type {{ pubkey?: string }} */ ({}),
},
}
+ if (osType === 'Linux') {
+ tauriPatch.tauri.bundle.resources = await linuxLibs(nativeDeps)
+ } else if (osType === 'Windows_NT') {
+ tauriPatch.tauri.bundle.resources = {
+ ...(await windowsDLLs(nativeDeps)),
+ [path.join(nativeDeps, 'models', 'yolov8s.onnx')]: './models/yolov8s.onnx',
+ }
+ }
+
+ // Location for desktop app tauri code
+ const tauriRoot = path.join(root, 'apps', 'desktop', 'src-tauri')
const tauriConfig = await fs
.readFile(path.join(tauriRoot, 'tauri.conf.json'), 'utf-8')
.then(JSON.parse)
@@ -138,5 +138,5 @@ export async function patchTauri(root, nativeDeps, targets, bundles, args) {
args.splice(1, 0, '-c', tauriPatchConf)
// Files to be removed
- return [tauriPatchConf, ...resources.toClean]
+ return [tauriPatchConf]
}
diff --git a/scripts/utils/shared.mjs b/scripts/utils/shared.mjs
index ad4ba3a88..65bf76d52 100644
--- a/scripts/utils/shared.mjs
+++ b/scripts/utils/shared.mjs
@@ -27,10 +27,15 @@ async function link(origin, target, rename) {
export async function symlinkSharedLibsLinux(root, nativeDeps) {
// rpath=${ORIGIN}/../lib/spacedrive
const targetLib = path.join(root, 'target', 'lib')
+ const targetShare = path.join(root, 'target', 'share', 'spacedrive')
const targetRPath = path.join(targetLib, 'spacedrive')
- await fs.unlink(targetRPath).catch(() => {})
- await fs.mkdir(targetLib, { recursive: true })
+ const targetModelShare = path.join(targetShare, 'models')
+ await Promise.all([
+ ...[targetRPath, targetModelShare].map(path => fs.unlink(path).catch(() => {})),
+ ...[targetLib, targetShare].map(path => fs.mkdir(path, { recursive: true })),
+ ])
await link(path.join(nativeDeps, 'lib'), targetRPath)
+ await link(path.join(nativeDeps, 'models'), targetModelShare)
}
/**
@@ -66,71 +71,40 @@ export async function symlinkSharedLibsMacOS(root, nativeDeps) {
/**
* Copy Windows DLLs for tauri build
- * @param {string} root
* @param {string} nativeDeps
- * @returns {Promise<{files: string[], toClean: string[]}>}
+ * @returns {Promise>}
*/
-export async function copyWindowsDLLs(root, nativeDeps) {
- const tauriSrc = path.join(root, 'apps', 'desktop', 'src-tauri')
- const files = await Promise.all(
- await fs.readdir(path.join(nativeDeps, 'bin'), { withFileTypes: true }).then(files =>
- files
- .filter(entry => entry.isFile() && entry.name.endsWith(`.dll`))
- .map(async entry => {
- await fs.copyFile(
- path.join(entry.path, entry.name),
- path.join(tauriSrc, entry.name)
- )
- return entry.name
- })
- )
+export async function windowsDLLs(nativeDeps) {
+ return Object.fromEntries(
+ await fs
+ .readdir(path.join(nativeDeps, 'bin'), { withFileTypes: true })
+ .then(files =>
+ files
+ .filter(entry => entry.isFile() && entry.name.endsWith(`.dll`))
+ .map(entry => [path.join(entry.path, entry.name), '.'])
+ )
)
-
- return { files, toClean: files.map(file => path.join(tauriSrc, file)) }
}
/**
* Symlink shared libs paths for Linux
- * @param {string} root
* @param {string} nativeDeps
- * @param {boolean} isDev
- * @returns {Promise<{files: string[], toClean: string[]}>}
+ * @returns {Promise>}
*/
-export async function copyLinuxLibs(root, nativeDeps, isDev) {
- // rpath=${ORIGIN}/../lib/spacedrive
- const tauriSrc = path.join(root, 'apps', 'desktop', 'src-tauri')
- const files = await fs
- .readdir(path.join(nativeDeps, 'lib'), { withFileTypes: true })
- .then(files =>
- Promise.all(
- files
- .filter(
- entry =>
- (entry.isFile() || entry.isSymbolicLink()) &&
- (entry.name.endsWith('.so') || entry.name.includes('.so.'))
- )
- .map(async entry => {
- if (entry.isSymbolicLink()) {
- await fs.symlink(
- await fs.readlink(path.join(entry.path, entry.name)),
- path.join(tauriSrc, entry.name)
- )
- } else {
- const target = path.join(tauriSrc, entry.name)
- await fs.copyFile(path.join(entry.path, entry.name), target)
- // https://web.archive.org/web/20220731055320/https://lintian.debian.org/tags/shared-library-is-executable
- await fs.chmod(target, 0o644)
- }
- return entry.name
- })
+export async function linuxLibs(nativeDeps) {
+ return Object.fromEntries(
+ await fs
+ .readdir(path.join(nativeDeps, 'lib'), { withFileTypes: true })
+ .then(files =>
+ Promise.all(
+ files
+ .filter(
+ entry =>
+ (entry.isFile() || entry.isSymbolicLink()) &&
+ (entry.name.endsWith('.so') || entry.name.includes('.so.'))
+ )
+ .map(entry => [path.join(entry.path, entry.name), '.'])
+ )
)
- )
-
- return {
- files,
- toClean: [
- ...files.map(file => path.join(tauriSrc, file)),
- ...files.map(file => path.join(root, 'target', isDev ? 'debug' : 'release', file)),
- ],
- }
+ )
}