@shaxpir/duiduidui-models 1.9.10 → 1.9.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/models/Device.d.ts +42 -0
- package/dist/models/Device.js +125 -1
- package/dist/models/SearchState.d.ts +50 -0
- package/dist/models/SearchState.js +71 -0
- package/dist/models/index.d.ts +1 -0
- package/dist/models/index.js +1 -0
- package/package.json +1 -1
package/dist/models/Device.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { ShareSync } from '../repo';
|
|
|
4
4
|
import { Content, ContentBody, ContentId, ContentMeta } from "./Content";
|
|
5
5
|
import { ConditionFilters } from './Condition';
|
|
6
6
|
import { BuiltInDbState, DatabaseVersion } from '../util/Database';
|
|
7
|
+
import { SearchHistory, SearchState } from './SearchState';
|
|
7
8
|
export interface LastSync {
|
|
8
9
|
at_utc_time: CompactDateTime | null;
|
|
9
10
|
}
|
|
@@ -37,6 +38,7 @@ export interface DevicePayload {
|
|
|
37
38
|
download_queue?: DownloadQueueItem[];
|
|
38
39
|
upload_queue?: UploadQueueItem[];
|
|
39
40
|
builtin_db?: BuiltInDbState;
|
|
41
|
+
search_history: SearchHistory;
|
|
40
42
|
}
|
|
41
43
|
export interface DeviceBody extends ContentBody {
|
|
42
44
|
meta: ContentMeta;
|
|
@@ -77,5 +79,45 @@ export declare class Device extends Content {
|
|
|
77
79
|
updateUploadQueueItem(predicate: (item: UploadQueueItem) => boolean, updates: Partial<BaseQueueItem>): void;
|
|
78
80
|
addImageToUploadQueue(imageId: ContentId): void;
|
|
79
81
|
removeImageFromUploadQueue(imageId: ContentId): void;
|
|
82
|
+
/**
|
|
83
|
+
* Gets the current search history.
|
|
84
|
+
*/
|
|
85
|
+
get searchHistory(): SearchHistory;
|
|
86
|
+
/**
|
|
87
|
+
* Gets the current search state (at the cursor position), or undefined if history is empty.
|
|
88
|
+
*/
|
|
89
|
+
get currentSearchState(): SearchState | undefined;
|
|
90
|
+
/**
|
|
91
|
+
* Returns true if back navigation is possible.
|
|
92
|
+
* Only returns true if the previous entry is non-empty (has query or conditions).
|
|
93
|
+
*/
|
|
94
|
+
get canGoBack(): boolean;
|
|
95
|
+
/**
|
|
96
|
+
* Returns true if forward navigation is possible.
|
|
97
|
+
*/
|
|
98
|
+
get canGoForward(): boolean;
|
|
99
|
+
/**
|
|
100
|
+
* Pushes a new search state onto the history stack.
|
|
101
|
+
* - Deduplicates: won't push if identical to current state
|
|
102
|
+
* - Discards forward history (entries after cursor)
|
|
103
|
+
* - Respects max entries limit (drops oldest when full)
|
|
104
|
+
*
|
|
105
|
+
* @returns The new search state at the cursor, or undefined if deduplicated
|
|
106
|
+
*/
|
|
107
|
+
pushSearchState(state: SearchState): SearchState | undefined;
|
|
108
|
+
/**
|
|
109
|
+
* Navigates back in search history.
|
|
110
|
+
* @returns The search state at the new cursor position, or undefined if can't go back
|
|
111
|
+
*/
|
|
112
|
+
goBack(): SearchState | undefined;
|
|
113
|
+
/**
|
|
114
|
+
* Navigates forward in search history.
|
|
115
|
+
* @returns The search state at the new cursor position, or undefined if can't go forward
|
|
116
|
+
*/
|
|
117
|
+
goForward(): SearchState | undefined;
|
|
118
|
+
/**
|
|
119
|
+
* Clears the entire search history stack.
|
|
120
|
+
*/
|
|
121
|
+
clearSearchHistory(): void;
|
|
80
122
|
}
|
|
81
123
|
export {};
|
package/dist/models/Device.js
CHANGED
|
@@ -7,6 +7,7 @@ const ArrayView_1 = require("./ArrayView");
|
|
|
7
7
|
const Content_1 = require("./Content");
|
|
8
8
|
const ContentKind_1 = require("./ContentKind");
|
|
9
9
|
const Operation_1 = require("./Operation");
|
|
10
|
+
const SearchState_1 = require("./SearchState");
|
|
10
11
|
class Device extends Content_1.Content {
|
|
11
12
|
static create(userId, deviceId) {
|
|
12
13
|
const now = shaxpir_common_1.ClockService.getClock().now();
|
|
@@ -29,7 +30,8 @@ class Device extends Content_1.Content {
|
|
|
29
30
|
download_status: 'none',
|
|
30
31
|
last_check: null,
|
|
31
32
|
last_error: null
|
|
32
|
-
}
|
|
33
|
+
},
|
|
34
|
+
search_history: (0, SearchState_1.createEmptySearchHistory)()
|
|
33
35
|
}
|
|
34
36
|
});
|
|
35
37
|
}
|
|
@@ -244,5 +246,127 @@ class Device extends Content_1.Content {
|
|
|
244
246
|
removeImageFromUploadQueue(imageId) {
|
|
245
247
|
this.removeFromUploadQueue(item => item.type === 'image' && item.image_id === imageId);
|
|
246
248
|
}
|
|
249
|
+
// Search History Methods
|
|
250
|
+
/**
|
|
251
|
+
* Gets the current search history.
|
|
252
|
+
*/
|
|
253
|
+
get searchHistory() {
|
|
254
|
+
this.checkDisposed("Device.searchHistory");
|
|
255
|
+
return this.payload.search_history;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Gets the current search state (at the cursor position), or undefined if history is empty.
|
|
259
|
+
*/
|
|
260
|
+
get currentSearchState() {
|
|
261
|
+
this.checkDisposed("Device.currentSearchState");
|
|
262
|
+
const history = this.searchHistory;
|
|
263
|
+
if (history.cursor < 0 || history.cursor >= history.entries.length) {
|
|
264
|
+
return undefined;
|
|
265
|
+
}
|
|
266
|
+
return history.entries[history.cursor];
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Returns true if back navigation is possible.
|
|
270
|
+
* Only returns true if the previous entry is non-empty (has query or conditions).
|
|
271
|
+
*/
|
|
272
|
+
get canGoBack() {
|
|
273
|
+
this.checkDisposed("Device.canGoBack");
|
|
274
|
+
const history = this.searchHistory;
|
|
275
|
+
if (history.cursor <= 0)
|
|
276
|
+
return false;
|
|
277
|
+
// Check if the entry we'd navigate to is non-empty
|
|
278
|
+
const prevEntry = history.entries[history.cursor - 1];
|
|
279
|
+
return !(0, SearchState_1.isEmptySearchState)(prevEntry);
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Returns true if forward navigation is possible.
|
|
283
|
+
*/
|
|
284
|
+
get canGoForward() {
|
|
285
|
+
this.checkDisposed("Device.canGoForward");
|
|
286
|
+
const history = this.searchHistory;
|
|
287
|
+
return history.cursor < history.entries.length - 1;
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Pushes a new search state onto the history stack.
|
|
291
|
+
* - Deduplicates: won't push if identical to current state
|
|
292
|
+
* - Discards forward history (entries after cursor)
|
|
293
|
+
* - Respects max entries limit (drops oldest when full)
|
|
294
|
+
*
|
|
295
|
+
* @returns The new search state at the cursor, or undefined if deduplicated
|
|
296
|
+
*/
|
|
297
|
+
pushSearchState(state) {
|
|
298
|
+
this.checkDisposed("Device.pushSearchState");
|
|
299
|
+
const history = this.searchHistory;
|
|
300
|
+
// Deduplicate: don't push if identical to current state
|
|
301
|
+
const currentState = history.cursor >= 0 && history.cursor < history.entries.length
|
|
302
|
+
? history.entries[history.cursor]
|
|
303
|
+
: undefined;
|
|
304
|
+
if ((0, SearchState_1.areSearchStatesEqual)(state, currentState)) {
|
|
305
|
+
return undefined;
|
|
306
|
+
}
|
|
307
|
+
// Build new entries array
|
|
308
|
+
// Discard any entries after the current cursor (browser-style)
|
|
309
|
+
let newEntries = history.cursor >= 0
|
|
310
|
+
? history.entries.slice(0, history.cursor + 1)
|
|
311
|
+
: [];
|
|
312
|
+
// Add the new state
|
|
313
|
+
newEntries.push(state);
|
|
314
|
+
// Enforce max entries limit - drop oldest entries
|
|
315
|
+
let newCursor = newEntries.length - 1;
|
|
316
|
+
if (newEntries.length > SearchState_1.SEARCH_HISTORY_MAX_ENTRIES) {
|
|
317
|
+
const overflow = newEntries.length - SearchState_1.SEARCH_HISTORY_MAX_ENTRIES;
|
|
318
|
+
newEntries = newEntries.slice(overflow);
|
|
319
|
+
newCursor = newEntries.length - 1;
|
|
320
|
+
}
|
|
321
|
+
// Commit the update
|
|
322
|
+
const batch = new Operation_1.BatchOperation(this);
|
|
323
|
+
batch.setPathValue(['payload', 'search_history'], {
|
|
324
|
+
entries: newEntries,
|
|
325
|
+
cursor: newCursor
|
|
326
|
+
});
|
|
327
|
+
batch.commit();
|
|
328
|
+
return state;
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Navigates back in search history.
|
|
332
|
+
* @returns The search state at the new cursor position, or undefined if can't go back
|
|
333
|
+
*/
|
|
334
|
+
goBack() {
|
|
335
|
+
this.checkDisposed("Device.goBack");
|
|
336
|
+
if (!this.canGoBack) {
|
|
337
|
+
return undefined;
|
|
338
|
+
}
|
|
339
|
+
const history = this.searchHistory;
|
|
340
|
+
const newCursor = history.cursor - 1;
|
|
341
|
+
const batch = new Operation_1.BatchOperation(this);
|
|
342
|
+
batch.setPathValue(['payload', 'search_history', 'cursor'], newCursor);
|
|
343
|
+
batch.commit();
|
|
344
|
+
return history.entries[newCursor];
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Navigates forward in search history.
|
|
348
|
+
* @returns The search state at the new cursor position, or undefined if can't go forward
|
|
349
|
+
*/
|
|
350
|
+
goForward() {
|
|
351
|
+
this.checkDisposed("Device.goForward");
|
|
352
|
+
if (!this.canGoForward) {
|
|
353
|
+
return undefined;
|
|
354
|
+
}
|
|
355
|
+
const history = this.searchHistory;
|
|
356
|
+
const newCursor = history.cursor + 1;
|
|
357
|
+
const batch = new Operation_1.BatchOperation(this);
|
|
358
|
+
batch.setPathValue(['payload', 'search_history', 'cursor'], newCursor);
|
|
359
|
+
batch.commit();
|
|
360
|
+
return history.entries[newCursor];
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Clears the entire search history stack.
|
|
364
|
+
*/
|
|
365
|
+
clearSearchHistory() {
|
|
366
|
+
this.checkDisposed("Device.clearSearchHistory");
|
|
367
|
+
const batch = new Operation_1.BatchOperation(this);
|
|
368
|
+
batch.setPathValue(['payload', 'search_history'], (0, SearchState_1.createEmptySearchHistory)());
|
|
369
|
+
batch.commit();
|
|
370
|
+
}
|
|
247
371
|
}
|
|
248
372
|
exports.Device = Device;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { ConditionFilters } from './Condition';
|
|
2
|
+
/**
|
|
3
|
+
* Represents the data portion of a search state.
|
|
4
|
+
* This is the shareable/persistable part of a search - it excludes
|
|
5
|
+
* transient UI state like SearchReason.
|
|
6
|
+
*
|
|
7
|
+
* Used for:
|
|
8
|
+
* - Search history entries in the Device model
|
|
9
|
+
* - Sharing search states between client and server
|
|
10
|
+
* - Cache keys and comparison
|
|
11
|
+
*/
|
|
12
|
+
export interface SearchState {
|
|
13
|
+
query?: string;
|
|
14
|
+
senseRank?: number;
|
|
15
|
+
conditions?: ConditionFilters;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Maximum number of entries in the search history stack.
|
|
19
|
+
* When this limit is reached, the oldest entry is dropped.
|
|
20
|
+
*/
|
|
21
|
+
export declare const SEARCH_HISTORY_MAX_ENTRIES = 50;
|
|
22
|
+
/**
|
|
23
|
+
* Search history with browser-style back/forward navigation.
|
|
24
|
+
*
|
|
25
|
+
* - `entries`: Stack of search states
|
|
26
|
+
* - `cursor`: Current position in the stack (0-indexed)
|
|
27
|
+
*
|
|
28
|
+
* Navigation behavior:
|
|
29
|
+
* - Back: decrements cursor (if cursor > 0)
|
|
30
|
+
* - Forward: increments cursor (if cursor < entries.length - 1)
|
|
31
|
+
* - New search: discards entries after cursor, pushes new entry, cursor moves to end
|
|
32
|
+
* - Clear: empties the entire stack
|
|
33
|
+
*/
|
|
34
|
+
export interface SearchHistory {
|
|
35
|
+
entries: SearchState[];
|
|
36
|
+
cursor: number;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Creates an empty search history.
|
|
40
|
+
*/
|
|
41
|
+
export declare function createEmptySearchHistory(): SearchHistory;
|
|
42
|
+
/**
|
|
43
|
+
* Checks if two SearchState objects are equivalent.
|
|
44
|
+
* Used for deduplication when pushing to history.
|
|
45
|
+
*/
|
|
46
|
+
export declare function areSearchStatesEqual(a: SearchState | undefined, b: SearchState | undefined): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Checks if a SearchState is empty (no query, no conditions).
|
|
49
|
+
*/
|
|
50
|
+
export declare function isEmptySearchState(state: SearchState | undefined): boolean;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SEARCH_HISTORY_MAX_ENTRIES = void 0;
|
|
4
|
+
exports.createEmptySearchHistory = createEmptySearchHistory;
|
|
5
|
+
exports.areSearchStatesEqual = areSearchStatesEqual;
|
|
6
|
+
exports.isEmptySearchState = isEmptySearchState;
|
|
7
|
+
/**
|
|
8
|
+
* Maximum number of entries in the search history stack.
|
|
9
|
+
* When this limit is reached, the oldest entry is dropped.
|
|
10
|
+
*/
|
|
11
|
+
exports.SEARCH_HISTORY_MAX_ENTRIES = 50;
|
|
12
|
+
/**
|
|
13
|
+
* Creates an empty search history.
|
|
14
|
+
*/
|
|
15
|
+
function createEmptySearchHistory() {
|
|
16
|
+
return {
|
|
17
|
+
entries: [],
|
|
18
|
+
cursor: -1
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Checks if two SearchState objects are equivalent.
|
|
23
|
+
* Used for deduplication when pushing to history.
|
|
24
|
+
*/
|
|
25
|
+
function areSearchStatesEqual(a, b) {
|
|
26
|
+
if (a === b)
|
|
27
|
+
return true;
|
|
28
|
+
if (!a || !b)
|
|
29
|
+
return false;
|
|
30
|
+
// Compare query (normalize empty string to undefined)
|
|
31
|
+
const queryA = a.query?.trim() || undefined;
|
|
32
|
+
const queryB = b.query?.trim() || undefined;
|
|
33
|
+
if (queryA !== queryB)
|
|
34
|
+
return false;
|
|
35
|
+
// Compare senseRank
|
|
36
|
+
if (a.senseRank !== b.senseRank)
|
|
37
|
+
return false;
|
|
38
|
+
// Compare conditions using JSON serialization (with sorted keys for consistency)
|
|
39
|
+
const conditionsA = a.conditions ? JSON.stringify(sortConditions(a.conditions)) : undefined;
|
|
40
|
+
const conditionsB = b.conditions ? JSON.stringify(sortConditions(b.conditions)) : undefined;
|
|
41
|
+
if (conditionsA !== conditionsB)
|
|
42
|
+
return false;
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Sorts condition arrays for consistent comparison.
|
|
47
|
+
*/
|
|
48
|
+
function sortConditions(conditions) {
|
|
49
|
+
const sorted = {};
|
|
50
|
+
if (conditions.any?.length) {
|
|
51
|
+
sorted.any = [...conditions.any].sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)));
|
|
52
|
+
}
|
|
53
|
+
if (conditions.all?.length) {
|
|
54
|
+
sorted.all = [...conditions.all].sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)));
|
|
55
|
+
}
|
|
56
|
+
if (conditions.none?.length) {
|
|
57
|
+
sorted.none = [...conditions.none].sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)));
|
|
58
|
+
}
|
|
59
|
+
return sorted;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Checks if a SearchState is empty (no query, no conditions).
|
|
63
|
+
*/
|
|
64
|
+
function isEmptySearchState(state) {
|
|
65
|
+
if (!state)
|
|
66
|
+
return true;
|
|
67
|
+
return !state.query?.trim() &&
|
|
68
|
+
!state.conditions?.any?.length &&
|
|
69
|
+
!state.conditions?.all?.length &&
|
|
70
|
+
!state.conditions?.none?.length;
|
|
71
|
+
}
|
package/dist/models/index.d.ts
CHANGED
package/dist/models/index.js
CHANGED
|
@@ -36,6 +36,7 @@ __exportStar(require("./Phrase"), exports);
|
|
|
36
36
|
__exportStar(require("./Profile"), exports);
|
|
37
37
|
__exportStar(require("./Progress"), exports);
|
|
38
38
|
__exportStar(require("./Review"), exports);
|
|
39
|
+
__exportStar(require("./SearchState"), exports);
|
|
39
40
|
__exportStar(require("./Session"), exports);
|
|
40
41
|
__exportStar(require("./SkillLevel"), exports);
|
|
41
42
|
__exportStar(require("./Social"), exports);
|