Преглед на файлове

feat: add auto-like functions to interface

Sv443 преди 10 месеца
родител
ревизия
2b668e4495
променени са 6 файла, в които са добавени 144 реда и са изтрити 32 реда
  1. 3 0
      changelog.md
  2. 76 4
      contributing.md
  3. 2 1
      src/dialogs/autoLike.ts
  4. 1 12
      src/features/input.ts
  5. 46 14
      src/interface.ts
  6. 16 1
      src/types.ts

+ 3 - 0
changelog.md

@@ -46,6 +46,9 @@
     - `showToast()` to show a custom toast notification with a message string or element and duration
     - `showToast()` to show a custom toast notification with a message string or element and duration
     - `showIconToast()` to show a custom toast notification with a message string or element, icon and duration
     - `showIconToast()` to show a custom toast notification with a message string or element, icon and duration
     - `createRipple()` to create a click ripple animation effect on a given element (experimental)
     - `createRipple()` to create a click ripple animation effect on a given element (experimental)
+  - Added functions:
+    - `getAutoLikeData()` to return the current auto-like data (authenticated function)
+    - `saveAutoLikeData()` to overwrite the auto-like data (authenticated function)
   - Added new SelectorObserver instance `browseResponse` for pages like `/channel/{id}`
   - Added new SelectorObserver instance `browseResponse` for pages like `/channel/{id}`
   - Added library `compare-versions` to the plugin interface at `unsafeWindow.BYTM.compareVersions` for easier plugin version comparison
   - Added library `compare-versions` to the plugin interface at `unsafeWindow.BYTM.compareVersions` for easier plugin version comparison
   - Added events
   - Added events

+ 76 - 4
contributing.md

@@ -298,10 +298,11 @@ An easy way to do this might be to include BetterYTM as a Git submodule, as long
 ### Global functions and classes:
 ### Global functions and classes:
 These are the global functions and classes that are exposed by BetterYTM through the `unsafeWindow.BYTM` object.  
 These are the global functions and classes that are exposed by BetterYTM through the `unsafeWindow.BYTM` object.  
 The usage and example blocks on each are written in TypeScript but can be used in JavaScript as well, after removing all type annotations.  
 The usage and example blocks on each are written in TypeScript but can be used in JavaScript as well, after removing all type annotations.  
+Functions marked with 🔒 need to be passed a per-session and per-plugin authentication token. It can be acquire by calling [registerPlugin()](#registerplugin)  
   
   
 - Meta:
 - Meta:
   - [registerPlugin()](#registerplugin) - Registers a plugin with BetterYTM with the given plugin definition object
   - [registerPlugin()](#registerplugin) - Registers a plugin with BetterYTM with the given plugin definition object
-  - [getPluginInfo()](#getplugininfo) - Returns the plugin info object for the specified plugin - also used to check if a certain plugin is registered
+  - [getPluginInfo()](#getplugininfo) 🔒 - Returns the plugin info object for the specified plugin - also used to check if a certain plugin is registered
 - BYTM-specific:
 - BYTM-specific:
   - [getResourceUrl()](#getresourceurl) - Returns a `blob:` URL provided by the local userscript extension for the specified BYTM resource file
   - [getResourceUrl()](#getresourceurl) - Returns a `blob:` URL provided by the local userscript extension for the specified BYTM resource file
   - [getSessionId()](#getsessionid) - Returns the unique session ID that is generated on every started session
   - [getSessionId()](#getsessionid) - Returns the unique session ID that is generated on every started session
@@ -321,20 +322,23 @@ The usage and example blocks on each are written in TypeScript but can be used i
   - [showIconToast()](#showicontoast) - Shows a toast notification with an icon and a message string or element
   - [showIconToast()](#showicontoast) - Shows a toast notification with an icon and a message string or element
   - [createRipple()](#createripple) - Creates a click ripple effect on the given element
   - [createRipple()](#createripple) - Creates a click ripple effect on the given element
 - Translations:
 - Translations:
-  - [setLocale()](#setlocale) - Sets the locale for BetterYTM
+  - [setLocale()](#setlocale) 🔒 - Sets the locale for BetterYTM
   - [getLocale()](#getlocale) - Returns the currently set locale
   - [getLocale()](#getlocale) - Returns the currently set locale
   - [hasKey()](#haskey) - Checks if the specified translation key exists in the currently set locale
   - [hasKey()](#haskey) - Checks if the specified translation key exists in the currently set locale
   - [hasKeyFor()](#haskeyfor) - Checks if the specified translation key exists in the specified locale
   - [hasKeyFor()](#haskeyfor) - Checks if the specified translation key exists in the specified locale
   - [t()](#t) - Translates the specified translation key using the currently set locale
   - [t()](#t) - Translates the specified translation key using the currently set locale
   - [tp()](#tp) - Translates the specified translation key including pluralization using the currently set locale
   - [tp()](#tp) - Translates the specified translation key including pluralization using the currently set locale
 - Feature config:
 - Feature config:
-  - [getFeatures()](#getfeatures) - Returns the current BYTM feature configuration object
-  - [saveFeatures()](#savefeatures) - Overwrites the current BYTM feature configuration object with the provided one
+  - [getFeatures()](#getfeatures) 🔒 - Returns the current BYTM feature configuration object
+  - [saveFeatures()](#savefeatures) 🔒 - Overwrites the current BYTM feature configuration object with the provided one
 - Lyrics:
 - Lyrics:
   - [fetchLyricsUrlTop()](#fetchlyricsurltop) - Fetches the URL to the lyrics page for the specified song
   - [fetchLyricsUrlTop()](#fetchlyricsurltop) - Fetches the URL to the lyrics page for the specified song
   - [getLyricsCacheEntry()](#getlyricscacheentry) - Tries to find a URL entry in the in-memory cache for the specified song
   - [getLyricsCacheEntry()](#getlyricscacheentry) - Tries to find a URL entry in the in-memory cache for the specified song
   - [sanitizeArtists()](#sanitizeartists) - Sanitizes the specified artist string to be used in fetching a lyrics URL
   - [sanitizeArtists()](#sanitizeartists) - Sanitizes the specified artist string to be used in fetching a lyrics URL
   - [sanitizeSong()](#sanitizesong) - Sanitizes the specified song title string to be used in fetching a lyrics URL
   - [sanitizeSong()](#sanitizesong) - Sanitizes the specified song title string to be used in fetching a lyrics URL
+- Auto-Like:
+  - [getAutoLikeData()](#getautolikedata) 🔒 - Returns the current auto-like data object
+  - [saveAutoLikeData()](#saveautolikedata) 🔒 - Overwrites the current auto-like data object with the provided one
 - Other:
 - Other:
   - [NanoEmitter](#nanoemitter) - Abstract class for creating lightweight, type safe event emitting classes
   - [NanoEmitter](#nanoemitter) - Abstract class for creating lightweight, type safe event emitting classes
 
 
@@ -1019,6 +1023,74 @@ The usage and example blocks on each are written in TypeScript but can be used i
 
 
 <br>
 <br>
 
 
+> #### getAutoLikeData()
+> Usage:  
+> ```ts
+> unsafeWindow.BYTM.getAutoLikeData(token: string | undefined): AutoLikeData
+> ```
+>   
+> Description:  
+> Returns the current auto-like data object synchronously from memory.  
+> To see the structure of the object, check out the type `AutoLikeData` in the file [`src/types.ts`](src/types.ts)
+>   
+> Arguments:
+> - `token` - The private token that was returned when the plugin was registered (if not provided, the function will return an empty object).
+>   
+> <details><summary><b>Example <i>(click to expand)</i></b></summary>
+> 
+> ```ts
+> const autoLikeData = unsafeWindow.BYTM.getAutoLikeData(myToken);
+> 
+> // check if the channel is added to the auto-like list and if it's currently enabled
+> function isEnabledForChannel(channelId: string) {
+>   return autoLikeData && autoLikeData.channels.find((ch) => ch.id === channelId && ch.enabled);
+> }
+> 
+> // channelId can be in the format UC... or @username
+> console.log(isEnabledForChannel("UCXuqSBlHAE6Xw-yeJA0Tunw"));
+> ```
+> </details>
+
+<br>
+
+> #### saveAutoLikeData()
+> Usage:  
+> ```ts
+> unsafeWindow.BYTM.saveAutoLikeData(token: string | undefined, data: AutoLikeData): Promise<void>
+> ```
+>   
+> Description:  
+> Saves the provided auto-like data object synchronously to memory and asynchronously to GM storage.  
+>   
+> Arguments:
+> - `token` - The private token that was returned when the plugin was registered (if not provided, the function will return an empty object).
+> - `data` - The full auto-like data object to save. No validation is done so if properties are missing, BYTM will break!
+>   
+> <details><summary><b>Example <i>(click to expand)</i></b></summary>
+> 
+> ```ts
+> async function toggleAutoLikeForChannel(channelId: string, channelName: string) {
+>   const autoLikeData = unsafeWindow.BYTM.getAutoLikeData(myToken);
+>   const channelIndex = autoLikeData.channels.findIndex((ch) => ch.id === channelId);
+> 
+>   if(channelIndex > -1)
+>     autoLikeData.channels[channelIndex].enabled = !autoLikeData.channels[channelIndex].enabled;
+>   else
+>     autoLikeData.channels.push({ id: channelId, name: channelName, enabled: true });
+> 
+>   await unsafeWindow.BYTM.saveAutoLikeData(myToken, autoLikeData);
+> }
+> 
+> // channelId can be in the format UC... or @username
+> toggleAutoLikeForChannel("UCXuqSBlHAE6Xw-yeJA0Tunw", "Linus Sex Tips").then(() => {
+>   const newAutoLikeData = unsafeWindow.BYTM.getAutoLikeData(myToken);
+>   console.log("Auto-like status for the channel was toggled. New data:", newAutoLikeData);
+> });
+> ```
+> </details>
+
+<br>
+
 > #### NanoEmitter
 > #### NanoEmitter
 > Usage:  
 > Usage:  
 > ```ts
 > ```ts

+ 2 - 1
src/dialogs/autoLike.ts

@@ -1,10 +1,11 @@
 import { compress, debounce } from "@sv443-network/userutils";
 import { compress, debounce } from "@sv443-network/userutils";
 import { compressionSupported, error, getDomain, log, onInteraction, parseChannelIdFromUrl, t, tryToDecompressAndParse } from "../utils/index.js";
 import { compressionSupported, error, getDomain, log, onInteraction, parseChannelIdFromUrl, t, tryToDecompressAndParse } from "../utils/index.js";
 import { BytmDialog, createCircularBtn, createToggleInput } from "../components/index.js";
 import { BytmDialog, createCircularBtn, createToggleInput } from "../components/index.js";
-import { autoLikeStore, initAutoLikeStore, type AutoLikeData } from "../features/index.js";
+import { autoLikeStore, initAutoLikeStore } from "../features/index.js";
 import { siteEvents } from "../siteEvents.js";
 import { siteEvents } from "../siteEvents.js";
 import { ImportExportDialog } from "../components/ImportExportDialog.js";
 import { ImportExportDialog } from "../components/ImportExportDialog.js";
 import { compressionFormat } from "../constants.js";
 import { compressionFormat } from "../constants.js";
+import type { AutoLikeData } from "../types.js";
 import "./autoLike.css";
 import "./autoLike.css";
 
 
 let autoLikeDialog: BytmDialog | null = null;
 let autoLikeDialog: BytmDialog | null = null;

+ 1 - 12
src/features/input.ts

@@ -1,6 +1,6 @@
 import { DataStore, clamp, compress, decompress } from "@sv443-network/userutils";
 import { DataStore, clamp, compress, decompress } from "@sv443-network/userutils";
 import { error, getVideoTime, info, log, warn, getVideoSelector, getDomain, compressionSupported, t, clearNode, resourceToHTMLString, getCurrentChannelId, currentMediaType } from "../utils/index.js";
 import { error, getVideoTime, info, log, warn, getVideoSelector, getDomain, compressionSupported, t, clearNode, resourceToHTMLString, getCurrentChannelId, currentMediaType } from "../utils/index.js";
-import type { Domain } from "../types.js";
+import type { AutoLikeData, Domain } from "../types.js";
 import { disableBeforeUnload } from "./behavior.js";
 import { disableBeforeUnload } from "./behavior.js";
 import { siteEvents } from "../siteEvents.js";
 import { siteEvents } from "../siteEvents.js";
 import { featInfo } from "./index.js";
 import { featInfo } from "./index.js";
@@ -150,17 +150,6 @@ export async function initNumKeysSkip() {
 
 
 let canCompress = false;
 let canCompress = false;
 
 
-export type AutoLikeData = {
-  channels: {
-    /** 24-character channel ID or user ID including the @ prefix */
-    id: string;
-    /** Channel name (for display purposes only) */
-    name: string;
-    /** Whether the channel should be auto-liked */
-    enabled: boolean;
-  }[];
-};
-
 /** DataStore instance for all auto-liked channels */
 /** DataStore instance for all auto-liked channels */
 export const autoLikeStore = new DataStore<AutoLikeData>({
 export const autoLikeStore = new DataStore<AutoLikeData>({
   id: "bytm-auto-like-channels",
   id: "bytm-auto-like-channels",

+ 46 - 14
src/interface.ts

@@ -4,9 +4,9 @@ import { mode, branch, host, buildNumber, compressionFormat, scriptInfo } from "
 import { getResourceUrl, getSessionId, getVideoTime, log, setLocale, getLocale, hasKey, hasKeyFor, NanoEmitter, t, tp, type TrLocale, info, error, onInteraction, getThumbnailUrl, getBestThumbnailUrl } from "./utils/index.js";
 import { getResourceUrl, getSessionId, getVideoTime, log, setLocale, getLocale, hasKey, hasKeyFor, NanoEmitter, t, tp, type TrLocale, info, error, onInteraction, getThumbnailUrl, getBestThumbnailUrl } from "./utils/index.js";
 import { addSelectorListener } from "./observers.js";
 import { addSelectorListener } from "./observers.js";
 import { getFeatures, setFeatures } from "./config.js";
 import { getFeatures, setFeatures } from "./config.js";
-import { featInfo, fetchLyricsUrlTop, getLyricsCacheEntry, sanitizeArtists, sanitizeSong } from "./features/index.js";
+import { autoLikeStore, featInfo, fetchLyricsUrlTop, getLyricsCacheEntry, sanitizeArtists, sanitizeSong } from "./features/index.js";
 import { allSiteEvents, type SiteEventsMap } from "./siteEvents.js";
 import { allSiteEvents, type SiteEventsMap } from "./siteEvents.js";
-import { LogLevel, type FeatureConfig, type FeatureInfo, type LyricsCacheEntry, type PluginDef, type PluginInfo, type PluginRegisterResult, type PluginDefResolvable, type PluginEventMap, type PluginItem, type BytmObject } from "./types.js";
+import { LogLevel, type FeatureConfig, type FeatureInfo, type LyricsCacheEntry, type PluginDef, type PluginInfo, type PluginRegisterResult, type PluginDefResolvable, type PluginEventMap, type PluginItem, type BytmObject, type AutoLikeData } from "./types.js";
 import { BytmDialog, createCircularBtn, createHotkeyInput, createRipple, createToggleInput, showIconToast, showToast } from "./components/index.js";
 import { BytmDialog, createCircularBtn, createHotkeyInput, createRipple, createToggleInput, showIconToast, showToast } from "./components/index.js";
 
 
 const { getUnsafeWindow, randomId } = UserUtils;
 const { getUnsafeWindow, randomId } = UserUtils;
@@ -107,36 +107,44 @@ export const allInterfaceEvents = [
 
 
 /** All functions that can be called on the BYTM interface using `unsafeWindow.BYTM.functionName();` (or `const { functionName } = unsafeWindow.BYTM;`) */
 /** All functions that can be called on the BYTM interface using `unsafeWindow.BYTM.functionName();` (or `const { functionName } = unsafeWindow.BYTM;`) */
 const globalFuncs = {
 const globalFuncs = {
-  // meta
+  // meta:
   registerPlugin,
   registerPlugin,
-  getPluginInfo,
+  /**/getPluginInfo,
 
 
-  // utils
-  addSelectorListener,
+  // bytm-specific:
   getResourceUrl,
   getResourceUrl,
   getSessionId,
   getSessionId,
+  // dom:
+  addSelectorListener,
+  onInteraction,
   getVideoTime,
   getVideoTime,
-  setLocale: setLocaleInterface,
+  getThumbnailUrl,
+  getBestThumbnailUrl,
+  // translations:
+  /**/setLocale: setLocaleInterface,
   getLocale,
   getLocale,
   hasKey,
   hasKey,
   hasKeyFor,
   hasKeyFor,
   t,
   t,
   tp,
   tp,
-  getFeatures: getFeaturesInterface,
-  saveFeatures: saveFeaturesInterface,
+  // feature config:
+  /**/getFeatures: getFeaturesInterface,
+  /**/saveFeatures: saveFeaturesInterface,
+  // lyrics:
   fetchLyricsUrlTop,
   fetchLyricsUrlTop,
   getLyricsCacheEntry,
   getLyricsCacheEntry,
   sanitizeArtists,
   sanitizeArtists,
   sanitizeSong,
   sanitizeSong,
-  onInteraction,
-  getThumbnailUrl,
-  getBestThumbnailUrl,
+  // auto-like:
+  /**/getAutoLikeData: getAutoLikeDataInterface,
+  /**/saveAutoLikeData: saveAutoLikeDataInterface,
+  // components:
   createHotkeyInput,
   createHotkeyInput,
   createToggleInput,
   createToggleInput,
   createCircularBtn,
   createCircularBtn,
+  createRipple,
   showToast,
   showToast,
   showIconToast,
   showIconToast,
-  createRipple,
 };
 };
 
 
 /** Initializes the BYTM interface */
 /** Initializes the BYTM interface */
@@ -216,6 +224,7 @@ export function initPlugins() {
       registeredPlugins.set(key, { def, events });
       registeredPlugins.set(key, { def, events });
       queuedPlugins.delete(key);
       queuedPlugins.delete(key);
       emitOnPlugins("pluginRegistered", (d) => sameDef(d, def), pluginDefToInfo(def)!);
       emitOnPlugins("pluginRegistered", (d) => sameDef(d, def), pluginDefToInfo(def)!);
+      info(`Initialized plugin '${getPluginKey(def)}'`, LogLevel.Info);
     }
     }
     catch(err) {
     catch(err) {
       error(`Failed to initialize plugin '${getPluginKey(def)}':`, err);
       error(`Failed to initialize plugin '${getPluginKey(def)}':`, err);
@@ -374,7 +383,10 @@ export function registerPlugin(def: PluginDef): PluginRegisterResult {
 
 
 /** Checks whether the passed token is a valid auth token for any registered plugin and returns the plugin ID, else returns undefined */
 /** Checks whether the passed token is a valid auth token for any registered plugin and returns the plugin ID, else returns undefined */
 export function resolveToken(token: string | undefined): string | undefined {
 export function resolveToken(token: string | undefined): string | undefined {
-  return token ? [...registeredPluginTokens.entries()].find(([, v]) => v === token)?.[0] ?? undefined : undefined;
+  return typeof token === "string" && token.length > 0
+    ? [...registeredPluginTokens.entries()]
+      .find(([, t]) => token === t)?.[0] ?? undefined
+    : undefined;
 }
 }
 
 
 //#region proxy funcs
 //#region proxy funcs
@@ -416,3 +428,23 @@ function saveFeaturesInterface(token: string | undefined, features: FeatureConfi
     return;
     return;
   setFeatures(features);
   setFeatures(features);
 }
 }
+
+/**
+ * Returns the auto-like data.  
+ * This is an authenticated function so you must pass the session- and plugin-unique token, retreived at registration.
+ */
+function getAutoLikeDataInterface(token: string | undefined) {
+  if(resolveToken(token) === undefined)
+    return;
+  return autoLikeStore.getData();
+}
+
+/**
+ * Saves new auto-like data, synchronously to the in-memory cache and asynchronously to the persistent storage.  
+ * This is an authenticated function so you must pass the session- and plugin-unique token, retreived at registration.
+ */
+function saveAutoLikeDataInterface(token: string | undefined, data: AutoLikeData) {
+  if(resolveToken(token) === undefined)
+    return;
+  return autoLikeStore.setData(data);
+}

+ 16 - 1
src/types.ts

@@ -8,6 +8,8 @@ import type { getFeatures, setFeatures } from "./config.js";
 import type { SiteEventsMap } from "./siteEvents.js";
 import type { SiteEventsMap } from "./siteEvents.js";
 import type { InterfaceEventsMap } from "./interface.js";
 import type { InterfaceEventsMap } from "./interface.js";
 
 
+//#region other
+
 /** Custom CLI args passed to rollup */
 /** Custom CLI args passed to rollup */
 export type RollupArgs = Partial<{
 export type RollupArgs = Partial<{
   "config-mode": "development" | "production";
   "config-mode": "development" | "production";
@@ -51,6 +53,17 @@ export type LyricsCacheEntry = {
   added: number;
   added: number;
 };
 };
 
 
+export type AutoLikeData = {
+  channels: {
+    /** 24-character channel ID or user ID including the @ prefix */
+    id: string;
+    /** Channel name (for display purposes only) */
+    name: string;
+    /** Whether the channel should be auto-liked */
+    enabled: boolean;
+  }[];
+};
+
 //#region global
 //#region global
 
 
 // shim for the BYTM interface properties
 // shim for the BYTM interface properties
@@ -222,7 +235,7 @@ export type InterfaceFunctions = {
   saveFeatures: typeof setFeatures;
   saveFeatures: typeof setFeatures;
 };
 };
 
 
-//#region features
+//#region feature defs
 
 
 export type FeatureKey = keyof FeatureConfig;
 export type FeatureKey = keyof FeatureConfig;
 
 
@@ -331,6 +344,8 @@ export type FeatureInfo = Record<
   & FeatureTypeProps
   & FeatureTypeProps
 >;
 >;
 
 
+//#region feature config
+
 /** Feature configuration */
 /** Feature configuration */
 export interface FeatureConfig {
 export interface FeatureConfig {
   //#region layout
   //#region layout