Explorar o código

feat: add auto-like functions to interface

Sv443 hai 10 meses
pai
achega
308d1e4cde
Modificáronse 6 ficheiros con 144 adicións e 32 borrados
  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
     - `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)
+  - 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 library `compare-versions` to the plugin interface at `unsafeWindow.BYTM.compareVersions` for easier plugin version comparison
   - 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:
 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.  
+Functions marked with 🔒 need to be passed a per-session and per-plugin authentication token. It can be acquire by calling [registerPlugin()](#registerplugin)  
   
 - Meta:
   - [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:
   - [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
@@ -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
   - [createRipple()](#createripple) - Creates a click ripple effect on the given element
 - Translations:
-  - [setLocale()](#setlocale) - Sets the locale for BetterYTM
+  - [setLocale()](#setlocale) 🔒 - Sets the locale for BetterYTM
   - [getLocale()](#getlocale) - Returns 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
   - [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
 - 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:
   - [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
   - [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
+- 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:
   - [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>
 
+> #### 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
 > Usage:  
 > ```ts

+ 2 - 1
src/dialogs/autoLike.ts

@@ -1,10 +1,11 @@
 import { compress, debounce } from "@sv443-network/userutils";
 import { compressionSupported, error, getDomain, log, onInteraction, parseChannelIdFromUrl, t, tryToDecompressAndParse } from "../utils/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 { ImportExportDialog } from "../components/ImportExportDialog.js";
 import { compressionFormat } from "../constants.js";
+import type { AutoLikeData } from "../types.js";
 import "./autoLike.css";
 
 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 { 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 { siteEvents } from "../siteEvents.js";
 import { featInfo } from "./index.js";
@@ -150,17 +150,6 @@ export async function initNumKeysSkip() {
 
 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 */
 export const autoLikeStore = new DataStore<AutoLikeData>({
   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 { addSelectorListener } from "./observers.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 { 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";
 
 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;`) */
 const globalFuncs = {
-  // meta
+  // meta:
   registerPlugin,
-  getPluginInfo,
+  /**/getPluginInfo,
 
-  // utils
-  addSelectorListener,
+  // bytm-specific:
   getResourceUrl,
   getSessionId,
+  // dom:
+  addSelectorListener,
+  onInteraction,
   getVideoTime,
-  setLocale: setLocaleInterface,
+  getThumbnailUrl,
+  getBestThumbnailUrl,
+  // translations:
+  /**/setLocale: setLocaleInterface,
   getLocale,
   hasKey,
   hasKeyFor,
   t,
   tp,
-  getFeatures: getFeaturesInterface,
-  saveFeatures: saveFeaturesInterface,
+  // feature config:
+  /**/getFeatures: getFeaturesInterface,
+  /**/saveFeatures: saveFeaturesInterface,
+  // lyrics:
   fetchLyricsUrlTop,
   getLyricsCacheEntry,
   sanitizeArtists,
   sanitizeSong,
-  onInteraction,
-  getThumbnailUrl,
-  getBestThumbnailUrl,
+  // auto-like:
+  /**/getAutoLikeData: getAutoLikeDataInterface,
+  /**/saveAutoLikeData: saveAutoLikeDataInterface,
+  // components:
   createHotkeyInput,
   createToggleInput,
   createCircularBtn,
+  createRipple,
   showToast,
   showIconToast,
-  createRipple,
 };
 
 /** Initializes the BYTM interface */
@@ -216,6 +224,7 @@ export function initPlugins() {
       registeredPlugins.set(key, { def, events });
       queuedPlugins.delete(key);
       emitOnPlugins("pluginRegistered", (d) => sameDef(d, def), pluginDefToInfo(def)!);
+      info(`Initialized plugin '${getPluginKey(def)}'`, LogLevel.Info);
     }
     catch(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 */
 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
@@ -416,3 +428,23 @@ function saveFeaturesInterface(token: string | undefined, features: FeatureConfi
     return;
   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 { InterfaceEventsMap } from "./interface.js";
 
+//#region other
+
 /** Custom CLI args passed to rollup */
 export type RollupArgs = Partial<{
   "config-mode": "development" | "production";
@@ -51,6 +53,17 @@ export type LyricsCacheEntry = {
   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
 
 // shim for the BYTM interface properties
@@ -222,7 +235,7 @@ export type InterfaceFunctions = {
   saveFeatures: typeof setFeatures;
 };
 
-//#region features
+//#region feature defs
 
 export type FeatureKey = keyof FeatureConfig;
 
@@ -331,6 +344,8 @@ export type FeatureInfo = Record<
   & FeatureTypeProps
 >;
 
+//#region feature config
+
 /** Feature configuration */
 export interface FeatureConfig {
   //#region layout