Spaces:
Paused
Paused
| import type { UserData } from '../db/schemas.js'; | |
| export type FieldType = 'list' | 'scalar'; | |
| export type FieldGroup = | |
| | 'branding' | |
| | 'filters' | |
| | 'sorting' | |
| | 'formatter' | |
| | 'proxy' | |
| | 'metadata' | |
| | 'misc'; | |
| export const MENU_IDS = [ | |
| 'about', | |
| 'services', | |
| 'addons', | |
| 'filters', | |
| 'sorting', | |
| 'formatter', | |
| 'proxy', | |
| 'miscellaneous', | |
| 'stats', | |
| 'save-install', | |
| ] as const; | |
| export type MenuId = (typeof MENU_IDS)[number]; | |
| export const FILTER_TAB_IDS = [ | |
| 'cache', | |
| 'resolution', | |
| 'quality', | |
| 'encode', | |
| 'stream-type', | |
| 'visual-tag', | |
| 'audio-tag', | |
| 'audio-channel', | |
| 'language', | |
| 'subtitle', | |
| 'seeders', | |
| 'age', | |
| 'matching', | |
| 'keyword', | |
| 'release-group', | |
| 'stream-expression', | |
| 'regex', | |
| 'size', | |
| 'bitrate', | |
| 'limit', | |
| 'deduplicator', | |
| 'miscellaneous', | |
| ] as const; | |
| export type FilterTabId = (typeof FILTER_TAB_IDS)[number]; | |
| export interface FieldMeta { | |
| label: string; | |
| group: FieldGroup; | |
| type: FieldType; | |
| /** For object-array list fields, the property used as the identity key for extend merging */ | |
| identityKey?: string; | |
| /** Top-level menu where this field lives (used for command palette navigation) */ | |
| menu: MenuId; | |
| /** Sub-tab within the menu (filters, miscellaneous, services, addons all use sub-tabs) */ | |
| subTab?: string; | |
| /** DOM id of the section/card to scroll to. Defaults to the field key when omitted. */ | |
| sectionId?: string; | |
| /** Extra search keywords for the command palette */ | |
| keywords?: string[]; | |
| /** Exclude from the parent config field-overrides UI. Field appears in the command palette but cannot be individually overridden in the parent config modal. */ | |
| ignoreForParentConfig?: boolean; | |
| } | |
| type IgnoredKeys = | |
| | 'uuid' | |
| | 'encryptedPassword' | |
| | 'trusted' | |
| | 'addons' | |
| | 'proxies' | |
| | 'ip' | |
| | 'addonCategories' | |
| | 'appliedTemplates' | |
| | 'precacheNextEpisode' | |
| | 'alwaysPrecache' | |
| | 'precacheCondition'; | |
| // prettier-ignore | |
| export const FIELD_META: Omit<Record<keyof UserData, FieldMeta>, IgnoredKeys> = { | |
| excludedResolutions: { label: 'Excluded Resolutions', group: 'filters', type: 'list', menu: 'filters', subTab: 'resolution' }, | |
| includedResolutions: { label: 'Included Resolutions', group: 'filters', type: 'list', menu: 'filters', subTab: 'resolution' }, | |
| requiredResolutions: { label: 'Required Resolutions', group: 'filters', type: 'list', menu: 'filters', subTab: 'resolution' }, | |
| preferredResolutions: { label: 'Preferred Resolutions', group: 'filters', type: 'list', menu: 'filters', subTab: 'resolution' }, | |
| excludedQualities: { label: 'Excluded Qualities', group: 'filters', type: 'list', menu: 'filters', subTab: 'quality' }, | |
| includedQualities: { label: 'Included Qualities', group: 'filters', type: 'list', menu: 'filters', subTab: 'quality' }, | |
| requiredQualities: { label: 'Required Qualities', group: 'filters', type: 'list', menu: 'filters', subTab: 'quality' }, | |
| preferredQualities: { label: 'Preferred Qualities', group: 'filters', type: 'list', menu: 'filters', subTab: 'quality' }, | |
| excludedLanguages: { label: 'Excluded Languages', group: 'filters', type: 'list', menu: 'filters', subTab: 'language' }, | |
| includedLanguages: { label: 'Included Languages', group: 'filters', type: 'list', menu: 'filters', subTab: 'language' }, | |
| requiredLanguages: { label: 'Required Languages', group: 'filters', type: 'list', menu: 'filters', subTab: 'language' }, | |
| preferredLanguages: { label: 'Preferred Languages', group: 'filters', type: 'list', menu: 'filters', subTab: 'language' }, | |
| excludedSubtitles: { label: 'Excluded Subtitles', group: 'filters', type: 'list', menu: 'filters', subTab: 'subtitle', keywords: ['subs'] }, | |
| includedSubtitles: { label: 'Included Subtitles', group: 'filters', type: 'list', menu: 'filters', subTab: 'subtitle', keywords: ['subs'] }, | |
| requiredSubtitles: { label: 'Required Subtitles', group: 'filters', type: 'list', menu: 'filters', subTab: 'subtitle', keywords: ['subs'] }, | |
| preferredSubtitles: { label: 'Preferred Subtitles', group: 'filters', type: 'list', menu: 'filters', subTab: 'subtitle', keywords: ['subs'] }, | |
| excludedVisualTags: { label: 'Excluded Visual Tags', group: 'filters', type: 'list', menu: 'filters', subTab: 'visual-tag', keywords: ['hdr', 'dolby vision', 'dv'] }, | |
| includedVisualTags: { label: 'Included Visual Tags', group: 'filters', type: 'list', menu: 'filters', subTab: 'visual-tag', keywords: ['hdr', 'dolby vision', 'dv'] }, | |
| requiredVisualTags: { label: 'Required Visual Tags', group: 'filters', type: 'list', menu: 'filters', subTab: 'visual-tag', keywords: ['hdr', 'dolby vision', 'dv'] }, | |
| preferredVisualTags: { label: 'Preferred Visual Tags', group: 'filters', type: 'list', menu: 'filters', subTab: 'visual-tag', keywords: ['hdr', 'dolby vision', 'dv'] }, | |
| excludedAudioTags: { label: 'Excluded Audio Tags', group: 'filters', type: 'list', menu: 'filters', subTab: 'audio-tag', keywords: ['atmos', 'dts', 'truehd'] }, | |
| includedAudioTags: { label: 'Included Audio Tags', group: 'filters', type: 'list', menu: 'filters', subTab: 'audio-tag', keywords: ['atmos', 'dts', 'truehd'] }, | |
| requiredAudioTags: { label: 'Required Audio Tags', group: 'filters', type: 'list', menu: 'filters', subTab: 'audio-tag', keywords: ['atmos', 'dts', 'truehd'] }, | |
| preferredAudioTags: { label: 'Preferred Audio Tags', group: 'filters', type: 'list', menu: 'filters', subTab: 'audio-tag', keywords: ['atmos', 'dts', 'truehd'] }, | |
| excludedAudioChannels: { label: 'Excluded Audio Channels', group: 'filters', type: 'list', menu: 'filters', subTab: 'audio-channel' }, | |
| includedAudioChannels: { label: 'Included Audio Channels', group: 'filters', type: 'list', menu: 'filters', subTab: 'audio-channel' }, | |
| requiredAudioChannels: { label: 'Required Audio Channels', group: 'filters', type: 'list', menu: 'filters', subTab: 'audio-channel' }, | |
| preferredAudioChannels: { label: 'Preferred Audio Channels', group: 'filters', type: 'list', menu: 'filters', subTab: 'audio-channel' }, | |
| excludedStreamTypes: { label: 'Excluded Stream Types', group: 'filters', type: 'list', menu: 'filters', subTab: 'stream-type' }, | |
| includedStreamTypes: { label: 'Included Stream Types', group: 'filters', type: 'list', menu: 'filters', subTab: 'stream-type' }, | |
| requiredStreamTypes: { label: 'Required Stream Types', group: 'filters', type: 'list', menu: 'filters', subTab: 'stream-type' }, | |
| preferredStreamTypes: { label: 'Preferred Stream Types', group: 'filters', type: 'list', menu: 'filters', subTab: 'stream-type' }, | |
| excludedEncodes: { label: 'Excluded Encodes', group: 'filters', type: 'list', menu: 'filters', subTab: 'encode' }, | |
| includedEncodes: { label: 'Included Encodes', group: 'filters', type: 'list', menu: 'filters', subTab: 'encode' }, | |
| requiredEncodes: { label: 'Required Encodes', group: 'filters', type: 'list', menu: 'filters', subTab: 'encode' }, | |
| preferredEncodes: { label: 'Preferred Encodes', group: 'filters', type: 'list', menu: 'filters', subTab: 'encode' }, | |
| excludedKeywords: { label: 'Excluded Keywords', group: 'filters', type: 'list', menu: 'filters', subTab: 'keyword' }, | |
| includedKeywords: { label: 'Included Keywords', group: 'filters', type: 'list', menu: 'filters', subTab: 'keyword' }, | |
| requiredKeywords: { label: 'Required Keywords', group: 'filters', type: 'list', menu: 'filters', subTab: 'keyword' }, | |
| preferredKeywords: { label: 'Preferred Keywords', group: 'filters', type: 'list', menu: 'filters', subTab: 'keyword' }, | |
| excludedReleaseGroups: { label: 'Excluded Release Groups', group: 'filters', type: 'list', menu: 'filters', subTab: 'release-group' }, | |
| includedReleaseGroups: { label: 'Included Release Groups', group: 'filters', type: 'list', menu: 'filters', subTab: 'release-group' }, | |
| requiredReleaseGroups: { label: 'Required Release Groups', group: 'filters', type: 'list', menu: 'filters', subTab: 'release-group' }, | |
| preferredReleaseGroups: { label: 'Preferred Release Groups', group: 'filters', type: 'list', menu: 'filters', subTab: 'release-group' }, | |
| excludedRegexPatterns: { label: 'Excluded Regex Patterns', group: 'filters', type: 'list', menu: 'filters', subTab: 'regex' }, | |
| includedRegexPatterns: { label: 'Included Regex Patterns', group: 'filters', type: 'list', menu: 'filters', subTab: 'regex' }, | |
| requiredRegexPatterns: { label: 'Required Regex Patterns', group: 'filters', type: 'list', menu: 'filters', subTab: 'regex' }, | |
| preferredRegexPatterns: { label: 'Preferred Regex Patterns', group: 'filters', type: 'list', identityKey: 'pattern', menu: 'filters', subTab: 'regex' }, | |
| rankedRegexPatterns: { label: 'Ranked Regex Patterns', group: 'filters', type: 'list', identityKey: 'pattern', menu: 'filters', subTab: 'regex' }, | |
| regexOverrides: { label: 'Regex Overrides', group: 'filters', type: 'list', identityKey: 'pattern', menu: 'filters', subTab: 'regex' }, | |
| syncedExcludedRegexUrls: { label: 'Synced Excluded Regex URLs', group: 'filters', type: 'list', menu: 'filters', subTab: 'regex' }, | |
| syncedIncludedRegexUrls: { label: 'Synced Included Regex URLs', group: 'filters', type: 'list', menu: 'filters', subTab: 'regex' }, | |
| syncedRequiredRegexUrls: { label: 'Synced Required Regex URLs', group: 'filters', type: 'list', menu: 'filters', subTab: 'regex' }, | |
| syncedPreferredRegexUrls: { label: 'Synced Preferred Regex URLs', group: 'filters', type: 'list', menu: 'filters', subTab: 'regex' }, | |
| syncedRankedRegexUrls: { label: 'Synced Ranked Regex URLs', group: 'filters', type: 'list', menu: 'filters', subTab: 'regex' }, | |
| excludedStreamExpressions: { label: 'Excluded Stream Expressions', group: 'filters', type: 'list', identityKey: 'expression', menu: 'filters', subTab: 'stream-expression', keywords: ['sel'] }, | |
| includedStreamExpressions: { label: 'Included Stream Expressions', group: 'filters', type: 'list', identityKey: 'expression', menu: 'filters', subTab: 'stream-expression', keywords: ['sel'] }, | |
| requiredStreamExpressions: { label: 'Required Stream Expressions', group: 'filters', type: 'list', identityKey: 'expression', menu: 'filters', subTab: 'stream-expression', keywords: ['sel'] }, | |
| preferredStreamExpressions: { label: 'Preferred Stream Expressions', group: 'filters', type: 'list', identityKey: 'expression', menu: 'filters', subTab: 'stream-expression', keywords: ['sel'] }, | |
| rankedStreamExpressions: { label: 'Ranked Stream Expressions', group: 'filters', type: 'list', identityKey: 'expression', menu: 'filters', subTab: 'stream-expression', keywords: ['sel'] }, | |
| selOverrides: { label: 'Stream Expression Overrides', group: 'filters', type: 'list', identityKey: 'expression', menu: 'filters', subTab: 'stream-expression' }, | |
| syncedExcludedStreamExpressionUrls: { label: 'Synced Excluded Expression URLs', group: 'filters', type: 'list', menu: 'filters', subTab: 'stream-expression' }, | |
| syncedIncludedStreamExpressionUrls: { label: 'Synced Included Expression URLs', group: 'filters', type: 'list', menu: 'filters', subTab: 'stream-expression' }, | |
| syncedRequiredStreamExpressionUrls: { label: 'Synced Required Expression URLs', group: 'filters', type: 'list', menu: 'filters', subTab: 'stream-expression' }, | |
| syncedPreferredStreamExpressionUrls: { label: 'Synced Preferred Expression URLs', group: 'filters', type: 'list', menu: 'filters', subTab: 'stream-expression' }, | |
| syncedRankedStreamExpressionUrls: { label: 'Synced Ranked Expression URLs', group: 'filters', type: 'list', menu: 'filters', subTab: 'stream-expression' }, | |
| enableSeadex: { label: 'SeaDex', group: 'filters', type: 'scalar', menu: 'filters', subTab: 'miscellaneous', keywords: ['anime', 'releases.moe'] }, | |
| excludeSeasonPacks: { label: 'Exclude Season Packs', group: 'filters', type: 'scalar', menu: 'filters', subTab: 'miscellaneous' }, | |
| excludeCached: { label: 'Exclude Cached Streams', group: 'filters', type: 'scalar', menu: 'filters', subTab: 'cache' }, | |
| excludeCachedFromAddons: { label: 'Exclude Cached — From Addons', group: 'filters', type: 'list', menu: 'filters', subTab: 'cache', sectionId: 'excludeCached' }, | |
| excludeCachedFromServices: { label: 'Exclude Cached — From Services', group: 'filters', type: 'list', menu: 'filters', subTab: 'cache', sectionId: 'excludeCached' }, | |
| excludeCachedFromStreamTypes: { label: 'Exclude Cached — Stream Types', group: 'filters', type: 'list', menu: 'filters', subTab: 'cache', sectionId: 'excludeCached' }, | |
| excludeCachedMode: { label: 'Exclude Cached Mode', group: 'filters', type: 'scalar', menu: 'filters', subTab: 'cache', sectionId: 'excludeCached' }, | |
| excludeUncached: { label: 'Exclude Uncached Streams', group: 'filters', type: 'scalar', menu: 'filters', subTab: 'cache' }, | |
| excludeUncachedFromAddons: { label: 'Exclude Uncached — From Addons', group: 'filters', type: 'list', menu: 'filters', subTab: 'cache', sectionId: 'excludeUncached' }, | |
| excludeUncachedFromServices: { label: 'Exclude Uncached — From Services', group: 'filters', type: 'list', menu: 'filters', subTab: 'cache', sectionId: 'excludeUncached' }, | |
| excludeUncachedFromStreamTypes: { label: 'Exclude Uncached — Stream Types', group: 'filters', type: 'list', menu: 'filters', subTab: 'cache', sectionId: 'excludeUncached' }, | |
| excludeUncachedMode: { label: 'Exclude Uncached Mode', group: 'filters', type: 'scalar', menu: 'filters', subTab: 'cache', sectionId: 'excludeUncached' }, | |
| excludeSeederRange: { label: 'Exclude Seeder Range', group: 'filters', type: 'scalar', menu: 'filters', subTab: 'seeders' }, | |
| includeSeederRange: { label: 'Include Seeder Range', group: 'filters', type: 'scalar', menu: 'filters', subTab: 'seeders' }, | |
| requiredSeederRange: { label: 'Required Seeder Range', group: 'filters', type: 'scalar', menu: 'filters', subTab: 'seeders' }, | |
| seederRangeTypes: { label: 'Seeder Range Types', group: 'filters', type: 'list', menu: 'filters', subTab: 'seeders' }, | |
| excludeAgeRange: { label: 'Exclude Age Range', group: 'filters', type: 'scalar', menu: 'filters', subTab: 'age' }, | |
| includeAgeRange: { label: 'Include Age Range', group: 'filters', type: 'scalar', menu: 'filters', subTab: 'age' }, | |
| requiredAgeRange: { label: 'Required Age Range', group: 'filters', type: 'scalar', menu: 'filters', subTab: 'age' }, | |
| ageRangeTypes: { label: 'Age Range Types', group: 'filters', type: 'list', menu: 'filters', subTab: 'age' }, | |
| digitalReleaseFilter: { label: 'Digital Release Filter', group: 'filters', type: 'scalar', menu: 'filters', subTab: 'miscellaneous' }, | |
| size: { label: 'Size Filter', group: 'filters', type: 'scalar', menu: 'filters', subTab: 'size' }, | |
| bitrate: { label: 'Bitrate Filter', group: 'filters', type: 'scalar', menu: 'filters', subTab: 'bitrate' }, | |
| titleMatching: { label: 'Title Matching', group: 'filters', type: 'scalar', menu: 'filters', subTab: 'matching' }, | |
| yearMatching: { label: 'Year Matching', group: 'filters', type: 'scalar', menu: 'filters', subTab: 'matching' }, | |
| seasonEpisodeMatching: { label: 'Season/Episode Matching', group: 'filters', type: 'scalar', menu: 'filters', subTab: 'matching' }, | |
| sortCriteria: { label: 'Sort Criteria', group: 'sorting', type: 'scalar', menu: 'sorting' }, | |
| deduplicator: { label: 'Deduplicator', group: 'sorting', type: 'scalar', menu: 'filters', subTab: 'deduplicator' }, | |
| resultLimits: { label: 'Result Limits', group: 'sorting', type: 'scalar', menu: 'filters', subTab: 'limit' }, | |
| formatter: { label: 'Formatter', group: 'formatter', type: 'scalar', menu: 'formatter' }, | |
| proxy: { label: 'Proxy', group: 'proxy', type: 'scalar', menu: 'proxy' }, | |
| tmdbApiKey: { label: 'TMDB API Key', group: 'metadata', type: 'scalar', menu: 'services', subTab: 'metadata', sectionId: 'tmdb' }, | |
| tmdbAccessToken: { label: 'TMDB Access Token', group: 'metadata', type: 'scalar', menu: 'services', subTab: 'metadata', sectionId : 'tmdb' }, | |
| tvdbApiKey: { label: 'TVDB API Key', group: 'metadata', type: 'scalar', menu: 'services', subTab: 'metadata' }, | |
| rpdbApiKey: { label: 'RPDB API Key', group: 'metadata', type: 'scalar', menu: 'services', subTab: 'posters' }, | |
| topPosterApiKey: { label: 'TopPoster API Key', group: 'metadata', type: 'scalar', menu: 'services', subTab: 'posters' }, | |
| aioratingsApiKey: { label: 'AIOratings API Key', group: 'metadata', type: 'scalar', menu: 'services', subTab: 'posters' }, | |
| aioratingsProfileId: { label: 'AIOratings Profile ID', group: 'metadata', type: 'scalar', menu: 'services', subTab: 'posters' }, | |
| openposterdbApiKey: { label: 'OpenPosterDB API Key', group: 'metadata', type: 'scalar', menu: 'services', subTab: 'posters' }, | |
| openposterdbUrl: { label: 'OpenPosterDB URL', group: 'metadata', type: 'scalar', menu: 'services', subTab: 'posters' }, | |
| openposterdbParameters: { label: 'OpenPosterDB Custom Parameters', group: 'metadata', type: 'scalar', menu: 'services', subTab: 'posters' }, | |
| posterService: { label: 'Poster Service', group: 'metadata', type: 'scalar', menu: 'services', subTab: 'posters' }, | |
| usePosterRedirectApi: { label: 'Use Poster Redirect API', group: 'metadata', type: 'scalar', menu: 'services', subTab: 'posters' }, | |
| usePosterServiceForMeta: { label: 'Use Poster Service for Meta', group: 'metadata', type: 'scalar', menu: 'services', subTab: 'posters' }, | |
| autoPlay: { label: 'Auto Play', group: 'misc', type: 'scalar', menu: 'miscellaneous', subTab: 'playback' }, | |
| areYouStillThere: { label: 'Are You Still There?', group: 'misc', type: 'scalar', menu: 'miscellaneous', subTab: 'playback' }, | |
| statistics: { label: 'Statistics', group: 'misc', type: 'scalar', menu: 'miscellaneous', subTab: 'display' }, | |
| hideErrors: { label: 'Hide Errors', group: 'misc', type: 'scalar', menu: 'miscellaneous', subTab: 'display' }, | |
| hideErrorsForResources: { label: 'Hide Errors for Resources', group: 'misc', type: 'list', menu: 'miscellaneous', subTab: 'display', sectionId: 'hideErrors' }, | |
| externalDownloads: { label: 'External Downloads', group: 'misc', type: 'scalar', menu: 'miscellaneous', subTab: 'display' }, | |
| preloadStreams: { label: 'Preload Streams', group: 'misc', type: 'scalar', menu: 'miscellaneous', subTab: 'background' }, | |
| precacheSelector: { label: 'Precache Selector', group: 'misc', type: 'scalar', menu: 'miscellaneous', subTab: 'background' }, | |
| precacheSingleStream: { label: 'Precache Single Stream', group: 'misc', type: 'scalar', menu: 'miscellaneous', subTab: 'background' }, | |
| dynamicAddonFetching: { label: 'Dynamic Addon Fetching', group: 'misc', type: 'scalar', menu: 'addons', subTab: 'addons', sectionId: 'fetchStrategy', keywords: ['exit condition', 'dynamic fetching', 'fetch strategy'] }, | |
| addonCategoryColors: { label: 'Addon Category Colors', group: 'misc', type: 'scalar', menu: 'addons', subTab: 'addons' }, | |
| catalogModifications: { label: 'Catalog Modifications', group: 'misc', type: 'scalar', menu: 'addons', subTab: 'catalogs' }, | |
| mergedCatalogs: { label: 'Merged Catalogs', group: 'misc', type: 'scalar', menu: 'addons', subTab: 'catalogs' }, | |
| nzbFailover: { label: 'NZB Failover', group: 'misc', type: 'scalar', menu: 'services', subTab: 'builtin' }, | |
| serviceWrap: { label: 'Service Wrap', group: 'misc', type: 'scalar', menu: 'services', subTab: 'builtin' }, | |
| cacheAndPlay: { label: 'Cache and Play', group: 'misc', type: 'scalar', menu: 'services', subTab: 'builtin' }, | |
| autoRemoveDownloads: { label: 'Auto Remove Downloads', group: 'misc', type: 'scalar', menu: 'services', subTab: 'builtin' }, | |
| checkOwned: { label: 'Check Owned', group: 'misc', type: 'scalar', menu: 'services', subTab: 'builtin' }, | |
| accessKey: { label: 'Config Access Key', group: 'misc', type: 'scalar', menu: 'save-install' }, | |
| showChanges: { label: 'Show Changes', group: 'misc', type: 'scalar', menu: 'save-install' }, | |
| addonName: { label: 'Addon Name', group: 'branding', type: 'scalar', menu: 'about', keywords: ['branding'] }, | |
| addonLogo: { label: 'Addon Logo', group: 'branding', type: 'scalar', menu: 'about', keywords: ['branding'] }, | |
| addonBackground: { label: 'Addon Background', group: 'branding', type: 'scalar', menu: 'about', keywords: ['branding'] }, | |
| addonDescription: { label: 'Addon Description', group: 'branding', type: 'scalar', menu: 'about', keywords: ['branding'] }, | |
| presets: { label: 'Addons', group: 'misc', type: 'list', identityKey: 'instanceId', menu: 'addons', subTab: 'addons', keywords: ['addons', 'presets'], ignoreForParentConfig: true }, | |
| services: { label: 'Services', group: 'misc', type: 'list', identityKey: 'id', menu: 'services', subTab: 'services', ignoreForParentConfig: true }, | |
| parentConfig: { label: 'Parent Config', group: 'misc', type: 'scalar', menu: 'miscellaneous', subTab: 'parent', sectionId: 'parentConfig', keywords: ['inherit', 'link', 'parent'], ignoreForParentConfig: true }, | |
| groups: { label: 'Groups', group: 'misc', type: 'scalar', menu: 'addons', subTab: 'addons', keywords: ['groupings'], ignoreForParentConfig: true, sectionId: 'fetchStrategy' }, | |
| }; | |