-
Notifications
You must be signed in to change notification settings - Fork 20
[WIP]: added smart dynamic button #964
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 15 commits
0a99b47
c81d596
00d57a9
a6f68c0
7c48db5
7c0da06
ce33a61
5d1363c
d6c3800
74abf22
b954a27
9b72667
11e8488
02c34dc
1ea2092
203a1a1
94ffbc2
0175528
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -185,6 +185,19 @@ export class TypedCollection<T extends Record<string, unknown>> { | |
| delete this._subsMap[id]; | ||
| } | ||
|
|
||
| public abort(id: Uid): void { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wish both And I really don't like the naming, |
||
| const item = this.read(id); | ||
| if (item?.getValue('isUploading')) { | ||
| this.remove(id); | ||
| } | ||
| } | ||
|
|
||
| public abortAll() { | ||
| this._items.forEach((id) => { | ||
| this.abort(id); | ||
| }); | ||
| } | ||
|
|
||
| public clearAll(): void { | ||
| this._items.forEach((id) => { | ||
| this.remove(id); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,131 @@ | ||
| import type { SourceButtonConfig } from '../../blocks/SourceBtn/SourceBtn'; | ||
| import type { PubSub } from '../../lit/PubSubCompat'; | ||
| import type { SharedState } from '../../lit/SharedState'; | ||
| import type { SharedInstancesBag } from '../../lit/shared-instances'; | ||
| import { stringToArray } from '../../utils/stringToArray'; | ||
| import type { PluginSourceRegistration } from '../managers/plugin'; | ||
| import { sharedConfigKey } from '../sharedConfigKey'; | ||
|
|
||
| export type SourceListControllerOptions = { | ||
| ctx: PubSub<SharedState>; | ||
| sharedInstancesBag: SharedInstancesBag; | ||
| onSourcesChange: (sources: SourceButtonConfig[]) => void; | ||
| }; | ||
|
|
||
| /** | ||
| * Controller that manages source list business logic. | ||
| * Handles source list configuration, plugin integration, and source expansion. | ||
| */ | ||
| export class SourceListController { | ||
| private _rawSourceList: string[] = []; | ||
| private _sources: SourceButtonConfig[] = []; | ||
| private _unsubscribePlugins?: () => void; | ||
| private _unsubscribeConfig?: () => void; | ||
| private _ctx: PubSub<SharedState>; | ||
| private _sharedInstancesBag: SharedInstancesBag; | ||
| private _onSourcesChange: (sources: SourceButtonConfig[]) => void; | ||
|
|
||
| public constructor(options: SourceListControllerOptions) { | ||
| this._ctx = options.ctx; | ||
| this._sharedInstancesBag = options.sharedInstancesBag; | ||
| this._onSourcesChange = options.onSourcesChange; | ||
| } | ||
|
|
||
| /** | ||
| * Initialize the controller and start listening to config and plugin changes | ||
| */ | ||
| public init(): void { | ||
| // Subscribe to sourceList config changes | ||
| this._unsubscribeConfig = this._ctx.sub(sharedConfigKey('sourceList'), (val: string) => { | ||
| this._rawSourceList = stringToArray(val); | ||
| this._updateSources(); | ||
| }); | ||
|
|
||
| // Subscribe to plugin changes | ||
| const pluginManager = this._sharedInstancesBag.pluginManager; | ||
|
|
||
| if (pluginManager?.onPluginsChange) { | ||
| this._unsubscribePlugins = pluginManager.onPluginsChange(() => this._updateSources()); | ||
| } | ||
|
|
||
| // Perform initial update | ||
| this._updateSources(); | ||
| } | ||
|
|
||
| /** | ||
| * Clean up subscriptions and resources | ||
| */ | ||
| public destroy(): void { | ||
| this._unsubscribePlugins?.(); | ||
| this._unsubscribePlugins = undefined; | ||
|
|
||
| this._unsubscribeConfig?.(); | ||
| this._unsubscribeConfig = undefined; | ||
| } | ||
|
|
||
| /** | ||
| * Get the current list of sources | ||
| */ | ||
| public getSources(): SourceButtonConfig[] { | ||
| return this._sources; | ||
| } | ||
|
|
||
| /** | ||
| * Update sources based on the current raw source list and available plugins | ||
| */ | ||
| private _updateSources(): void { | ||
| const pluginManager = this._sharedInstancesBag.pluginManager; | ||
| const pluginSources = pluginManager?.snapshot().sources ?? []; | ||
| const pluginSourceById = new Map(pluginSources.map((source) => [source.id, source])); | ||
|
|
||
| const sources: SourceButtonConfig[] = []; | ||
|
|
||
| this._rawSourceList.forEach((srcName) => { | ||
| const expanded = this._expandSource(srcName, pluginSourceById); | ||
|
|
||
| // If expansion returned different entries (e.g., camera -> mobile modes), resolve them | ||
| const expandedDiffer = expanded.length !== 1 || expanded[0] !== srcName; | ||
| if (expandedDiffer) { | ||
| for (const name of expanded) { | ||
| const pluginSource = pluginSourceById.get(name); | ||
| if (pluginSource) { | ||
| sources.push(this._makePluginSourceConfig(pluginSource)); | ||
| } | ||
| } | ||
| return; | ||
| } | ||
|
|
||
| const pluginSource = pluginSourceById.get(srcName); | ||
| if (pluginSource) { | ||
| sources.push(this._makePluginSourceConfig(pluginSource)); | ||
| } | ||
| }); | ||
|
|
||
| this._sources = sources; | ||
| this._onSourcesChange(this._sources); | ||
| } | ||
|
|
||
| /** | ||
| * Expand a source name into one or more source IDs | ||
| */ | ||
| private _expandSource(srcName: string, pluginSourceById: Map<string, PluginSourceRegistration>): string[] { | ||
| const pluginSource = pluginSourceById.get(srcName); | ||
| if (pluginSource?.expand) { | ||
| return pluginSource.expand(); | ||
| } | ||
|
|
||
| return [srcName]; | ||
| } | ||
|
|
||
| /** | ||
| * Convert a plugin source registration into a source button config | ||
| */ | ||
| private _makePluginSourceConfig(source: PluginSourceRegistration): SourceButtonConfig { | ||
| return { | ||
| id: source.id, | ||
| label: source.label, | ||
| icon: source.icon, | ||
| onClick: () => source.onSelect(), | ||
| }; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export * from './SourceListController'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,7 +14,19 @@ export class ClipboardLayer extends SharedInstance { | |
| window.addEventListener('paste', this.listener); | ||
| } | ||
|
|
||
| private _excludingNodes(target: Element) { | ||
| if (target.closest('uc-url-source')) { | ||
| return true; | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
|
|
||
| private openUploadList() { | ||
| if (this._sharedInstancesBag.smartBtn.shouldReturnToSmartButtonAfterFileAdd()) { | ||
| return; | ||
| } | ||
|
|
||
| this._sharedInstancesBag.api.setCurrentActivity(ACTIVITY_TYPES.UPLOAD_LIST); | ||
| this._sharedInstancesBag.api.setModalState(true); | ||
|
Comment on lines
25
to
31
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Delegate clipboard follow-up to In smart-button mode this now does nothing after paste: it neither opens the upload list nor runs the "return to smart button" path. Other entry points in this PR use Suggested change private openUploadList() {
- if (this._sharedInstancesBag.smartBtn.shouldReturnToSmartButtonAfterFileAdd()) {
- return;
- }
-
- this._sharedInstancesBag.api.setCurrentActivity(ACTIVITY_TYPES.UPLOAD_LIST);
- this._sharedInstancesBag.api.setModalState(true);
+ this._sharedInstancesBag.smartBtn.showUploadListAfterFileAdd();
}🤖 Prompt for AI Agents |
||
| } | ||
|
|
@@ -29,12 +41,16 @@ export class ClipboardLayer extends SharedInstance { | |
| continue; | ||
| } | ||
|
|
||
| if (this._excludingNodes(event.target as Element)) { | ||
| return; | ||
| } | ||
|
|
||
| switch (this._cfg.pasteScope) { | ||
| case 'global': | ||
| await this.handlePaste(event); | ||
| break; | ||
| case 'local': | ||
| if (!scope.contains(event.target as Node)) { | ||
| if (!scope.contains(event.target as Element)) { | ||
|
Comment on lines
+44
to
+53
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Guard On a window paste listener, Suggested change- if (this._excludingNodes(event.target as Element)) {
+ const target = event.target;
+ if (target instanceof Element && this._excludingNodes(target)) {
return;
}
switch (this._cfg.pasteScope) {
case 'global':
await this.handlePaste(event);
break;
case 'local':
- if (!scope.contains(event.target as Element)) {
+ if (!(target instanceof Node) || !scope.contains(target)) {
continue;
}
await this.handlePaste(event);
break;🤖 Prompt for AI Agents |
||
| continue; | ||
| } | ||
| await this.handlePaste(event); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| import { ACTIVITY_TYPES } from '../../lit/activity-constants'; | ||
| import { findBlockInCtx } from '../../lit/findBlockInCtx'; | ||
| import { SharedInstance } from '../../lit/shared-instances'; | ||
|
|
||
| type SmartBtnSolutionBlock = { | ||
| isSmartBtnActive: boolean; | ||
| }; | ||
|
|
||
| export class SmartBtnLayer extends SharedInstance { | ||
| public get isActive(): boolean { | ||
| const solutionBlock = findBlockInCtx( | ||
| this._sharedInstancesBag.blocksRegistry, | ||
| (block) => 'isSmartBtnActive' in block, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What if inverse logic a bit? Like rename this layer to something abstract like Like in SmartBtn we're doing like And inside RouterHooksLayer we iterate over those hooks and fallback to upload list if none of them returned true. |
||
| ) as SmartBtnSolutionBlock | undefined; | ||
|
|
||
| return solutionBlock?.isSmartBtnActive ?? false; | ||
| } | ||
|
|
||
| public shouldReturnToSmartButtonAfterFileAdd(): boolean { | ||
| const history = this._sharedInstancesBag.ctx.read('*history'); | ||
|
|
||
| return this.isActive && history.length === 0; | ||
| } | ||
|
|
||
| public showUploadListAfterFileAdd(): void { | ||
| if (this.shouldReturnToSmartButtonAfterFileAdd()) { | ||
| this._returnToSmartButton(); | ||
| return; | ||
| } | ||
|
|
||
| this._sharedInstancesBag.ctx.pub('*currentActivity', ACTIVITY_TYPES.UPLOAD_LIST); | ||
| this._sharedInstancesBag.modalManager?.open(ACTIVITY_TYPES.UPLOAD_LIST); | ||
| } | ||
|
|
||
| private _returnToSmartButton(): void { | ||
| const currentActivity = this._sharedInstancesBag.ctx.read('*currentActivity'); | ||
|
|
||
| if (currentActivity) { | ||
| this._sharedInstancesBag.modalManager?.close(currentActivity); | ||
| } else { | ||
| this._sharedInstancesBag.modalManager?.closeAll(); | ||
| } | ||
|
|
||
| this._sharedInstancesBag.ctx.pub('*currentActivity', null); | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.