├── src
├── pages
│ ├── preview
│ │ └── save-to-library-modal.ts
│ ├── portfolio
│ │ └── index.ts
│ ├── publish
│ │ └── publish-page.ts
│ ├── cta-sdk-page.ts
│ ├── home-page.ts.css
│ ├── compress-assets-page.ts
│ ├── home-page.ts
│ ├── folder-size
│ │ ├── folder-tree-view.ts
│ │ └── folder-treemap-view.ts
│ ├── base64-converter.ts
│ └── imba-packer-page.ts
├── fw
│ ├── layout-component-base.ts
│ ├── component-base.ts
│ ├── index.ts
│ ├── from-query.ts
│ ├── image-popup.ts
│ ├── di.ts
│ ├── update-notification.ts
│ ├── version-checker.ts
│ └── router.ts
├── services
│ ├── PreviewServiceValidators
│ │ ├── index.ts
│ │ ├── types.ts
│ │ ├── GeneralValidator.ts
│ │ ├── FacebookValidator.ts
│ │ └── MraidValidator.ts
│ ├── MetadataService.ts
│ ├── types.ts
│ ├── Base64ConverterService.ts
│ └── ImbaPackerService.ts
├── vite-env.d.ts
├── assets
│ ├── lit.svg
│ ├── preview-presets.json
│ ├── cta-sdk.md
│ └── platforms-config.json
├── utils
│ ├── url-utils.ts
│ └── AsstesExtractor.ts
├── Layout
│ ├── gamepad-icon.svg
│ ├── site-logo.ts
│ ├── nav-menu.ts
│ └── main-layout.ts
├── app-root.ts
├── theme.css
└── sw-version-handler.js
├── public
├── publish-data
│ ├── TikTok
│ │ └── config.json
│ ├── cta.Facebook.js
│ ├── cta.Facebook_Zip.js
│ ├── cta.Moloco.js
│ ├── cta.AdColony.js
│ ├── cta.Applovin.js
│ ├── cta.Mraid2.js
│ ├── cta.Mintegral.js
│ ├── cta.Google.js
│ ├── cta.TikTok.js
│ ├── cta.Vungle.js
│ ├── cta.dv360.js
│ ├── cta.Unity.js
│ └── cta.IronSource.js
├── pwa.png
├── big-logo.jpg
├── PngChpocker.png
├── files
│ ├── base.apk
│ └── PngChpocker.zip
├── large-image.jpg
├── small-logo.jpg
├── version.json
├── backgrounds
│ ├── back2.svg
│ ├── back1.svg
│ ├── checkboard.svg
│ └── dark-checkboard.svg
├── manifest.webmanifest
├── PlayableTools
│ └── playable-tools.svg
├── test-simple-playable.html
├── gamepad.svg
├── playable-tools.svg
├── zip-preview-sw.js
├── playable-screenshot.js
├── fb_validator.js
└── mraid.js
├── media
├── pwa.png
├── big-logo.jpg
├── large-image.jpg
├── large-image.psd
├── small-logo.jpg
├── backgrounds
│ ├── back1.jpg
│ ├── back2.jpg
│ ├── back3.jpg
│ ├── back4.jpg
│ ├── back2.svg
│ ├── back1.svg
│ ├── checkboard.svg
│ └── dark-checkboard.svg
├── app-screenshots
│ ├── base64.png
│ └── previewer.jpg
└── playable-screenshots
│ └── LBR_Construct3d.png
├── TestPlayableSources
├── TestFacebookCta.c3p
├── FacebookMinimalTest
│ ├── facebook_test.zip
│ └── facebook_test.html
└── Construct3
│ └── Construct3PlayableTest_Facebook.zip
├── .gitignore
├── .env.local.example
├── .htaccess-example
├── nginx-version-config.example
├── vite-plugin-rewrite-base-href.ts
├── package.json
├── tsconfig.json
├── vite-plugin-yandex-metrika.ts
├── test-version-fetch.js
├── test-facebook-validator.html
├── .github
└── workflows
│ └── static.yml
├── vite-plugin-version.ts
├── vite.config.ts
├── test-playable.html
├── index.html
├── README.md
└── VERSION_CHECKING.md
/src/pages/preview/save-to-library-modal.ts:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/pages/portfolio/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./portfolio-page";
2 |
--------------------------------------------------------------------------------
/public/publish-data/TikTok/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "playable_orientation": 0
3 | }
--------------------------------------------------------------------------------
/media/pwa.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gritsenko/PlayableTools/main/media/pwa.png
--------------------------------------------------------------------------------
/public/pwa.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gritsenko/PlayableTools/main/public/pwa.png
--------------------------------------------------------------------------------
/media/big-logo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gritsenko/PlayableTools/main/media/big-logo.jpg
--------------------------------------------------------------------------------
/public/big-logo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gritsenko/PlayableTools/main/public/big-logo.jpg
--------------------------------------------------------------------------------
/media/large-image.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gritsenko/PlayableTools/main/media/large-image.jpg
--------------------------------------------------------------------------------
/media/large-image.psd:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gritsenko/PlayableTools/main/media/large-image.psd
--------------------------------------------------------------------------------
/media/small-logo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gritsenko/PlayableTools/main/media/small-logo.jpg
--------------------------------------------------------------------------------
/public/PngChpocker.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gritsenko/PlayableTools/main/public/PngChpocker.png
--------------------------------------------------------------------------------
/public/files/base.apk:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gritsenko/PlayableTools/main/public/files/base.apk
--------------------------------------------------------------------------------
/public/large-image.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gritsenko/PlayableTools/main/public/large-image.jpg
--------------------------------------------------------------------------------
/public/small-logo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gritsenko/PlayableTools/main/public/small-logo.jpg
--------------------------------------------------------------------------------
/media/backgrounds/back1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gritsenko/PlayableTools/main/media/backgrounds/back1.jpg
--------------------------------------------------------------------------------
/media/backgrounds/back2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gritsenko/PlayableTools/main/media/backgrounds/back2.jpg
--------------------------------------------------------------------------------
/media/backgrounds/back3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gritsenko/PlayableTools/main/media/backgrounds/back3.jpg
--------------------------------------------------------------------------------
/media/backgrounds/back4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gritsenko/PlayableTools/main/media/backgrounds/back4.jpg
--------------------------------------------------------------------------------
/public/files/PngChpocker.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gritsenko/PlayableTools/main/public/files/PngChpocker.zip
--------------------------------------------------------------------------------
/media/app-screenshots/base64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gritsenko/PlayableTools/main/media/app-screenshots/base64.png
--------------------------------------------------------------------------------
/public/version.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "1.4.4-dev",
3 | "buildTime": "2025-12-10T08:15:14.136Z",
4 | "hash": "fb9d35ff"
5 | }
--------------------------------------------------------------------------------
/media/app-screenshots/previewer.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gritsenko/PlayableTools/main/media/app-screenshots/previewer.jpg
--------------------------------------------------------------------------------
/TestPlayableSources/TestFacebookCta.c3p:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gritsenko/PlayableTools/main/TestPlayableSources/TestFacebookCta.c3p
--------------------------------------------------------------------------------
/media/playable-screenshots/LBR_Construct3d.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gritsenko/PlayableTools/main/media/playable-screenshots/LBR_Construct3d.png
--------------------------------------------------------------------------------
/src/fw/layout-component-base.ts:
--------------------------------------------------------------------------------
1 | import { ComponentBase } from "./component-base";
2 |
3 | export class LayoutComponentBase extends ComponentBase {
4 | }
5 |
--------------------------------------------------------------------------------
/TestPlayableSources/FacebookMinimalTest/facebook_test.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gritsenko/PlayableTools/main/TestPlayableSources/FacebookMinimalTest/facebook_test.zip
--------------------------------------------------------------------------------
/TestPlayableSources/Construct3/Construct3PlayableTest_Facebook.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gritsenko/PlayableTools/main/TestPlayableSources/Construct3/Construct3PlayableTest_Facebook.zip
--------------------------------------------------------------------------------
/public/publish-data/cta.Facebook.js:
--------------------------------------------------------------------------------
1 | var scriptElt = { "type": "ok" }; //to avoid script error in Facebook
2 | document.CTA = {
3 | onClick: function () {
4 | FbPlayableAd.onCTAClick();
5 | window.console.log("CTA Clicked");
6 | }
7 | };
8 | document._xrq_ = window[atob("WE1MSHR0cHBSZXF1ZXN0")];
9 |
--------------------------------------------------------------------------------
/public/publish-data/cta.Facebook_Zip.js:
--------------------------------------------------------------------------------
1 | var scriptElt = { "type": "ok" }; //to avoid script error in Facebook
2 | document.CTA = {
3 | onClick: function () {
4 | FbPlayableAd.onCTAClick();
5 | window.console.log("CTA Clicked");
6 | }
7 | };
8 | document._xrq_ = window[atob("WE1MSHR0cHBSZXF1ZXN0")];
9 |
--------------------------------------------------------------------------------
/public/publish-data/cta.Moloco.js:
--------------------------------------------------------------------------------
1 | var scriptElt = { "type": "ok" }; //to avoid script error in Facebook
2 | document.CTA = {
3 | onClick: function () {
4 | FbPlayableAd.onCTAClick();
5 | window.console.log("CTA Clicked");
6 | }
7 | };
8 |
9 | document._xrq_ = window[atob("WE1MSHR0cHBSZXF1ZXN0")];
10 |
--------------------------------------------------------------------------------
/src/services/PreviewServiceValidators/index.ts:
--------------------------------------------------------------------------------
1 | export { GeneralValidator } from './GeneralValidator';
2 | export { FacebookValidator } from './FacebookValidator';
3 | export { MraidValidator } from './MraidValidator';
4 | export type { Validator, ValidationResult, ValidationCategory, ValidationCheck, ValidatorFactory } from './types';
--------------------------------------------------------------------------------
/media/backgrounds/back2.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/backgrounds/back2.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/media/backgrounds/back1.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/backgrounds/back1.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/fw/component-base.ts:
--------------------------------------------------------------------------------
1 | import { LitElement } from 'lit';
2 |
3 | export class ComponentBase extends LitElement {
4 | protected static useShadowDom = false;
5 | // Override createRenderRoot to respect the `useShadowDom` setting
6 | createRenderRoot() {
7 | if ((this.constructor as typeof ComponentBase).useShadowDom) {
8 | return super.createRenderRoot(); // Shadow DOM
9 | } else {
10 | return this; // Light DOM
11 | }
12 | }
13 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist-ssr
12 | *.local
13 |
14 | dev-dist
15 | dist
16 |
17 | # Environment variables - NEVER commit these files
18 | .env
19 | .env.local
20 | .env.*.local
21 | .env.production
22 |
23 | # Editor directories and files
24 | .vscode/*
25 | !.vscode/extensions.json
26 | .idea
27 | .DS_Store
28 | *.suo
29 | *.ntvs*
30 | *.njsproj
31 | *.sln
32 | *.sw?
33 |
--------------------------------------------------------------------------------
/public/publish-data/cta.AdColony.js:
--------------------------------------------------------------------------------
1 | document.CTA = {
2 | onClick: function (store) {
3 |
4 | if (store === undefined)
5 | store = navigator.userAgent.toLowerCase().indexOf("android") > -1 ? "google" : "apple";
6 |
7 | var urls = {
8 | "google": "{{google}}",
9 | "apple": "{{apple}}"
10 | };
11 | var url = urls[store];
12 | window.console.log("CTA Clicked store: " + store + " link: " + url);
13 | mraid.open(url);
14 | }
15 | };
--------------------------------------------------------------------------------
/public/publish-data/cta.Applovin.js:
--------------------------------------------------------------------------------
1 | document.CTA = {
2 | onClick: function (store) {
3 |
4 | if (store === undefined)
5 | store = navigator.userAgent.toLowerCase().indexOf("android") > -1 ? "google" : "apple";
6 |
7 | var urls = {
8 | "google": "{{google}}",
9 | "apple": "{{apple}}"
10 | };
11 | var url = urls[store];
12 | window.console.log("CTA Clicked store: " + store + " link: " + url);
13 | mraid.open(url);
14 | }
15 | };
--------------------------------------------------------------------------------
/public/publish-data/cta.Mraid2.js:
--------------------------------------------------------------------------------
1 | document.CTA = {
2 | onClick: function (store) {
3 |
4 | if (store === undefined)
5 | store = navigator.userAgent.toLowerCase().indexOf("android") > -1 ? "google" : "apple";
6 |
7 | var urls = {
8 | "google": "{{google}}",
9 | "apple": "{{apple}}"
10 | };
11 | var url = urls[store];
12 | window.console.log("CTA Clicked store: " + store + " link: " + url);
13 | mraid.open(url);
14 | }
15 | };
--------------------------------------------------------------------------------
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | interface ImportMetaEnv {
4 | readonly VITE_FIREBASE_API_KEY: string;
5 | readonly VITE_FIREBASE_AUTH_DOMAIN: string;
6 | readonly VITE_FIREBASE_PROJECT_ID: string;
7 | readonly VITE_FIREBASE_STORAGE_BUCKET: string;
8 | readonly VITE_FIREBASE_MESSAGING_SENDER_ID: string;
9 | readonly VITE_FIREBASE_APP_ID: string;
10 | readonly VITE_FIREBASE_DATABASE_URL: string;
11 | }
12 |
13 | interface ImportMeta {
14 | readonly env: ImportMetaEnv;
15 | }
16 |
--------------------------------------------------------------------------------
/.env.local.example:
--------------------------------------------------------------------------------
1 | # Firebase Configuration
2 | # Copy this file to .env.local and fill in your actual Firebase credentials
3 | # .env.local is git-ignored and should never be committed
4 |
5 | VITE_FIREBASE_API_KEY=your_firebase_api_key_here
6 | VITE_FIREBASE_AUTH_DOMAIN=your_auth_domain_here
7 | VITE_FIREBASE_PROJECT_ID=your_project_id_here
8 | VITE_FIREBASE_STORAGE_BUCKET=your_storage_bucket_here
9 | VITE_FIREBASE_MESSAGING_SENDER_ID=your_messaging_sender_id_here
10 | VITE_FIREBASE_APP_ID=your_app_id_here
11 | VITE_FIREBASE_DATABASE_URL=your_database_url_here
12 |
--------------------------------------------------------------------------------
/src/services/PreviewServiceValidators/types.ts:
--------------------------------------------------------------------------------
1 | export interface ValidationCheck {
2 | name: string;
3 | passed: boolean;
4 | details?: string;
5 | isWarning?: boolean;
6 | }
7 |
8 | export interface ValidationCategory {
9 | name: string;
10 | checks: ValidationCheck[];
11 | }
12 |
13 | export interface ValidationResult {
14 | categories: ValidationCategory[];
15 | }
16 |
17 | export interface Validator {
18 | validate(content: string, fileSize: number): ValidationResult;
19 | }
20 |
21 | export interface ValidatorFactory {
22 | create(): Validator;
23 | }
--------------------------------------------------------------------------------
/.htaccess-example:
--------------------------------------------------------------------------------
1 | # Prevent caching of version.json file
2 |
3 | Header set Cache-Control "no-cache, no-store, must-revalidate"
4 | Header set Pragma "no-cache"
5 | Header set Expires "0"
6 | Header set ETag ""
7 | Header unset Last-Modified
8 |
9 |
10 | # Alternative using regex pattern
11 |
12 | Header set Cache-Control "no-cache, no-store, must-revalidate"
13 | Header set Pragma "no-cache"
14 | Header set Expires "0"
15 | Header set ETag ""
16 | Header unset Last-Modified
17 |
18 |
--------------------------------------------------------------------------------
/public/manifest.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Gritsenko Playable Tools",
3 | "short_name": "PlayableTools",
4 | "start_url": "/",
5 | "display": "standalone",
6 | "background_color": "#ffffff",
7 | "theme_color": "#3b82f6",
8 | "description": "Open-source tools for HTML5 playable ads developers.",
9 | "icons": [
10 | {
11 | "src": "/playable-tools.svg",
12 | "sizes": "192x192",
13 | "type": "image/svg+xml"
14 | },
15 | {
16 | "src": "/playable-tools.svg",
17 | "sizes": "512x512",
18 | "type": "image/svg+xml"
19 | }
20 | ]
21 | }
22 |
--------------------------------------------------------------------------------
/public/PlayableTools/playable-tools.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/publish-data/cta.Mintegral.js:
--------------------------------------------------------------------------------
1 | document.CTA = {
2 | onClick: function (store) {
3 | window.install && window.install();
4 | },
5 |
6 | gameEnd: function () {
7 | window.gameEnd && window.gameEnd();
8 | },
9 |
10 | gameReady: function () {
11 | window.gameReady && window.gameReady();
12 | }
13 |
14 | };
15 |
16 | window.addEventListener('load', (event) => {
17 | window.gameReady && window.gameReady();
18 | });
19 |
20 | function gameStart() {
21 | console.log("Game started");
22 | }
23 |
24 | function gameClose() {
25 | console.log("Game closed");
26 | }
--------------------------------------------------------------------------------
/src/assets/lit.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/nginx-version-config.example:
--------------------------------------------------------------------------------
1 | # Nginx configuration to prevent caching of version.json
2 | # Add this to your server block or location block
3 |
4 | location ~* /version\.json$ {
5 | add_header Cache-Control "no-cache, no-store, must-revalidate" always;
6 | add_header Pragma "no-cache" always;
7 | add_header Expires "0" always;
8 | add_header ETag "" always;
9 |
10 | # Remove any existing ETag or Last-Modified headers
11 | more_clear_headers "ETag";
12 | more_clear_headers "Last-Modified";
13 |
14 | # Ensure the file is served with proper MIME type
15 | add_header Content-Type "application/json" always;
16 | }
17 |
--------------------------------------------------------------------------------
/src/fw/index.ts:
--------------------------------------------------------------------------------
1 | import { customElement, property, state } from 'lit/decorators.js';
2 | import { inject } from './di';
3 | import { route } from './router';
4 | import { html, css } from 'lit';
5 |
6 | import {fromQuery} from './from-query';
7 |
8 | export * from './component-base';
9 | export * from './di';
10 | export * from './router';
11 | export * from './layout-component-base';
12 | export * from './image-popup';
13 | export * from './update-notification';
14 | export * from './version-checker';
15 |
16 | export {
17 | html,
18 | css,
19 | customElement,
20 | property,
21 | state,
22 | inject,
23 | route,
24 | fromQuery
25 | };
26 |
--------------------------------------------------------------------------------
/media/backgrounds/checkboard.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/backgrounds/checkboard.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/media/backgrounds/dark-checkboard.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/backgrounds/dark-checkboard.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/services/MetadataService.ts:
--------------------------------------------------------------------------------
1 | export interface PageMetadata {
2 | title?: string;
3 | description?: string;
4 | }
5 |
6 | class MetadataService {
7 | update(metadata: PageMetadata) {
8 | if (metadata.title) {
9 | document.title = metadata.title;
10 | }
11 |
12 | let descriptionEl = document.querySelector('meta[name="description"]');
13 | if (!descriptionEl) {
14 | descriptionEl = document.createElement('meta');
15 | descriptionEl.setAttribute('name', 'description');
16 | document.head.appendChild(descriptionEl);
17 | }
18 | descriptionEl.setAttribute('content', metadata.description || '');
19 | }
20 | }
21 |
22 | export const metadataService = new MetadataService();
23 |
--------------------------------------------------------------------------------
/vite-plugin-rewrite-base-href.ts:
--------------------------------------------------------------------------------
1 | import { Plugin } from 'vite';
2 | import fs from 'fs';
3 | import path from 'path';
4 |
5 | export default function rewriteBaseHrefPlugin(): Plugin {
6 | let outDir = 'dist';
7 | return {
8 | name: 'rewrite-base-href',
9 | configResolved(resolvedConfig) {
10 | outDir = resolvedConfig.build?.outDir || 'dist';
11 | },
12 | closeBundle() {
13 | const indexPath = path.resolve(process.cwd(), outDir, 'index.html');
14 | if (fs.existsSync(indexPath)) {
15 | let html = fs.readFileSync(indexPath, 'utf8');
16 | html = html.replace('', '');
17 | fs.writeFileSync(indexPath, html, 'utf8');
18 | }
19 | },
20 | };
21 | }
22 |
--------------------------------------------------------------------------------
/public/publish-data/cta.Google.js:
--------------------------------------------------------------------------------
1 | var headNode = document.getElementsByTagName("head")[0],
2 | script = document.createElement("script");
3 |
4 | // script attribute
5 | script.setAttribute("type", "text/javascript");
6 | script.setAttribute("charset", "utf-8");
7 | script.setAttribute("src", "//tpc.googlesyndication.com/pagead/gadgets/html5/api/exitapi.js");
8 | // inject elements
9 | headNode.appendChild(script);
10 |
11 | script.onload = function () {
12 | ExitApi.delayCloseButton(0);
13 | };
14 |
15 | document.CTA = {
16 | onClick: function () {
17 | try {
18 | ExitApi.exit();
19 | } catch (b) {
20 | console.log(b);
21 | }
22 | window.console.log("CTA Clicked");
23 | }
24 | };
--------------------------------------------------------------------------------
/public/publish-data/cta.TikTok.js:
--------------------------------------------------------------------------------
1 | var headNode = document.getElementsByTagName("head")[0],
2 | script = document.createElement("script");
3 |
4 | // script attribute
5 | script.setAttribute("type", "text/javascript");
6 | script.setAttribute("charset", "utf-8");
7 | script.setAttribute("src", "https://sf16-muse-va.ibytedtos.com/obj/union-fe-nc-i18n/playable/sdk/playable-sdk.js");
8 | // inject elements
9 | headNode.appendChild(script);
10 |
11 | script.onload = function () {
12 | //sdk api loaded
13 | };
14 |
15 | document.CTA = {
16 | onClick: function () {
17 | try {
18 | window.playableSDK.openAppStore();
19 | } catch (b) {
20 | console.log(b);
21 | }
22 | window.console.log("CTA Clicked");
23 | }
24 | };
--------------------------------------------------------------------------------
/src/utils/url-utils.ts:
--------------------------------------------------------------------------------
1 | // Utility for URL rewriting and subfolder deployment support
2 | export class UrlUtils {
3 | /**
4 | * Returns the base directory URL for subfolder deployments, ensuring trailing slash.
5 | */
6 | static getBaseDir(): string {
7 | const basePath = window.location.origin + window.location.pathname.replace(/([?#].*)$/, "");
8 | return basePath.endsWith("/") ? basePath : basePath + "/";
9 | }
10 |
11 | /**
12 | * Builds a fetch URL for a resource in a subfolder deployment.
13 | * @param subPath Subfolder path (e.g., 'publish-data/')
14 | * @param resource Resource filename (e.g., 'cta.Facebook.js')
15 | */
16 | static buildFetchUrl(subPath: string, resource: string): string {
17 | return this.getBaseDir() + subPath + resource;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "playable_tools",
3 | "private": true,
4 | "version": "1.4.4",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "tsc && vite build",
9 | "preview": "vite preview"
10 | },
11 | "dependencies": {
12 | "@picocss/pico": "^2.1.1",
13 | "@types/d3": "^7.4.3",
14 | "@types/jszip": "^3.4.0",
15 | "d3": "^7.9.0",
16 | "fw": "file:./src/fw",
17 | "jszip": "^3.10.1",
18 | "lit": "^3.3.0",
19 | "marked": "^16.0.0",
20 | "pako": "^2.1.0",
21 | "reflect-metadata": "^0.2.2",
22 | "vite-plugin-pwa": "^1.0.1",
23 | "vite-plugin-string": "^1.2.3"
24 | },
25 | "devDependencies": {
26 | "@types/node": "^24.0.13",
27 | "@types/pako": "^2.0.3",
28 | "typescript": "~5.8.3",
29 | "vite": "^7.0.4"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/public/publish-data/cta.Vungle.js:
--------------------------------------------------------------------------------
1 | var headNode = document.getElementsByTagName("head")[0],
2 | script = document.createElement("script");
3 |
4 | // script attribute
5 | script.setAttribute("type", "text/javascript");
6 | script.setAttribute("charset", "utf-8");
7 | script.setAttribute("src", "mraid.js");
8 | // inject elements
9 | headNode.appendChild(script);
10 |
11 | document.CTA = {
12 | onClick: function (store) {
13 |
14 | if (store === undefined)
15 | store = navigator.userAgent.toLowerCase().indexOf("android") > -1 ? "google" : "apple";
16 |
17 | var urls = {
18 | "google": "{{google}}",
19 | "apple": "{{apple}}"
20 | };
21 | var url = urls[store];
22 | window.console.log("CTA Clicked store: " + store + " link: " + url);
23 | mraid.open(url);
24 | }
25 | };
--------------------------------------------------------------------------------
/src/fw/from-query.ts:
--------------------------------------------------------------------------------
1 | // Decorator to inject value from hash-based query string
2 | export function fromQuery(paramName: string) {
3 | return function (target: any, propertyKey: string) {
4 | const getter = function () {
5 | const hash = window.location.hash;
6 | if (hash) {
7 | const queryIndex = hash.indexOf('?');
8 | if (queryIndex !== -1) {
9 | const query = hash.substring(queryIndex + 1);
10 | const params = new URLSearchParams(query);
11 | return params.get(paramName);
12 | }
13 | }
14 | return null;
15 | };
16 | Object.defineProperty(target, propertyKey, {
17 | get: getter,
18 | enumerable: true,
19 | configurable: true
20 | });
21 | };
22 | }
23 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2022",
4 | "experimentalDecorators": true,
5 | "emitDecoratorMetadata": true,
6 | "useDefineForClassFields": false,
7 | "module": "ESNext",
8 | "lib": ["ES2022", "DOM", "DOM.Iterable"],
9 | "skipLibCheck": true,
10 |
11 | /* Bundler mode */
12 | "moduleResolution": "bundler",
13 | "allowImportingTsExtensions": true,
14 | "verbatimModuleSyntax": true,
15 | "moduleDetection": "force",
16 | "noEmit": true,
17 |
18 | /* Linting */
19 | "strict": true,
20 | "noUnusedLocals": true,
21 | "noUnusedParameters": true,
22 | "erasableSyntaxOnly": true,
23 | "noFallthroughCasesInSwitch": true,
24 | "noUncheckedSideEffectImports": true,
25 | "baseUrl": ".",
26 | "paths": {
27 | "fw": ["src/fw/index.ts"],
28 | "fw/*": ["src/fw/*"]
29 | }
30 | },
31 | "include": ["src", "src/cta-sdk.md.d.ts"]
32 | }
33 |
--------------------------------------------------------------------------------
/public/publish-data/cta.dv360.js:
--------------------------------------------------------------------------------
1 | var headNode = document.getElementsByTagName("head")[0],
2 | script = document.createElement("script");
3 |
4 | // script attribute
5 | script.setAttribute("type", "text/javascript");
6 | script.setAttribute("charset", "utf-8");
7 | script.setAttribute("src", "https://s0.2mdn.net/ads/studio/Enabler.js");
8 | // inject elements
9 | headNode.appendChild(script);
10 |
11 | var clickTag = "http://my.com";
12 |
13 | window.onload = function () {
14 | if (Enabler.isInitialized()) {
15 | enablerInitHandler();
16 | } else {
17 | Enabler.addEventListener(studio.events.StudioEvent.INIT, enablerInitHandler);
18 | }
19 | }
20 |
21 | function enablerInitHandler() {
22 |
23 | }
24 |
25 | function bgExitHandler(e) {
26 | Enabler.exit('Background Exit');
27 | }
28 |
29 | document.CTA = {
30 | onClick: function () {
31 | bgExitHandler();
32 | window.console.log("CTA Clicked");
33 | }
34 | };
--------------------------------------------------------------------------------
/src/pages/publish/publish-page.ts:
--------------------------------------------------------------------------------
1 | import { ComponentBase, customElement, html, route as route } from "fw";
2 | import "./playable-publisher";
3 |
4 | @customElement("publish-page")
5 | @route("/publish", {
6 | title: "Publish Playable Ads",
7 | description: "Publish your playable ads to multiple ad networks with ease. This tool streamlines the process of deploying your ads."
8 | })
9 | export class HomePage extends ComponentBase {
10 | render() {
11 | return html`
12 |
13 |
14 |
Important: You must integrate the CTA SDK into your playable ad for successful publishing. See
cta-sdk for instructions.
15 |
16 |
17 |
18 | `;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/pages/cta-sdk-page.ts:
--------------------------------------------------------------------------------
1 | import { ComponentBase, customElement, html, route } from "fw";
2 | import { unsafeHTML } from "lit-html/directives/unsafe-html.js";
3 | import { marked } from "marked";
4 | // @ts-ignore
5 | import markdownContent from "../assets/cta-sdk.md?raw";
6 |
7 | @customElement("cta-sdk-page")
8 | @route("/cta-sdk", {
9 | title: "CTA SDK Documentation",
10 | description: "Documentation for the CTA SDK, providing guidance on how to integrate and use the SDK in your playable ads.",
11 | })
12 | export class CtaSdkPage extends ComponentBase {
13 | markdownHtml: string = "";
14 |
15 | connectedCallback() {
16 | super.connectedCallback();
17 |
18 | const content = marked.parse(markdownContent);
19 | if (typeof content === "string") {
20 | this.markdownHtml = content;
21 | }
22 |
23 | this.requestUpdate();
24 | }
25 |
26 | render() {
27 | return html`
28 |
29 |
30 | ${unsafeHTML(this.markdownHtml)}
31 |
32 |
33 | `;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/TestPlayableSources/FacebookMinimalTest/facebook_test.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Facebook CTA Test
7 |
27 |
28 |
29 |
30 |
Facebook CTA Test
31 |
32 | Click the button below to trigger
33 | FbPlayableAd.onCTAClick(). This page includes a lightweight
34 | mock so it can be used locally.
35 |
36 |
37 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/src/assets/preview-presets.json:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | {
4 | "id": "default",
5 | "name": "Default Preview",
6 | "description": "Standard preview without any ad network validation",
7 | "maxFileSizeMB": 10,
8 | "injectScripts": [],
9 | "replaceTokens": {}
10 | },
11 | {
12 | "id": "facebook",
13 | "name": "Facebook Validator",
14 | "description": "Facebook ad network validation with content security policies",
15 | "maxFileSizeMB": 5,
16 | "injectScripts": [
17 | {
18 | "source": "fb_validator.js",
19 | "position": "beforeHeadEnd"
20 | }
21 | ],
22 | "replaceTokens": {
23 | "XMLHttpRequest": "_xrq_"
24 | }
25 | }
26 | ,
27 | {
28 | "id": "mraid",
29 | "name": "MRAID Preview",
30 | "description": "Preview configured for MRAID playables. Useful when testing playables targeted to ad networks that use MRAID (examples: Unity, IronSource, AppLovin, Vungle, Mintegral, MoPub). Injects mraid.js where appropriate and preserves MRAID API behavior when available.",
31 | "maxFileSizeMB": 10,
32 | "injectScripts": [
33 | {
34 | "source": "mraid.js",
35 | "position": "afterBodyStart"
36 | }
37 | ],
38 | "replaceTokens": {}
39 | }
40 | ]
41 | }
42 |
--------------------------------------------------------------------------------
/src/services/types.ts:
--------------------------------------------------------------------------------
1 | export interface PlatformConfig {
2 | Name: string;
3 | InjeectScripts?: string[];
4 | format?: string;
5 | ExtractScripts?: boolean;
6 | OutputIndexHtmlName?: string;
7 | ExtraFiles?: { from: string; to: string }[];
8 | Sizes?: Record;
9 | replaceTokens?: Record;
10 | }
11 |
12 | export interface PlayableProcessOptions {
13 | name?: string;
14 | title?: string;
15 | googlePlayUrl?: string;
16 | appStoreUrl?: string;
17 | suffix?: string;
18 | outputDirectory?: FileSystemDirectoryHandle;
19 | onProgress?: (progress: number, platform?: string) => void;
20 | // Add more options as needed
21 | }
22 |
23 | export interface PreviewScript {
24 | source: string;
25 | position: 'beforeHeadEnd' | 'afterBodyStart' | 'beforeBodyEnd';
26 | }
27 |
28 | export interface PreviewPreset {
29 | id: string;
30 | name: string;
31 | description: string;
32 | maxFileSizeMB: number;
33 | injectScripts: PreviewScript[];
34 | replaceTokens: Record;
35 | }
36 |
37 | export interface PreviewPresetsConfig {
38 | presets: PreviewPreset[];
39 | }
40 |
41 | export interface PreviewServiceOptions {
42 | preset?: PreviewPreset;
43 | customMaxFileSizeMB?: number;
44 | customInjectScripts?: PreviewScript[];
45 | customReplaceTokens?: Record;
46 | }
47 |
--------------------------------------------------------------------------------
/vite-plugin-yandex-metrika.ts:
--------------------------------------------------------------------------------
1 | import type { Plugin } from 'vite';
2 | import fs from 'fs';
3 | import path from 'path';
4 |
5 | const METRIKA_COMMENT = '';
6 | const METRIKA_SNIPPET = `\n\n
\n`;
7 |
8 | export default function yandexMetrikaPlugin(): Plugin {
9 | return {
10 | name: 'inject-yandex-metrika',
11 | apply: 'build',
12 | enforce: 'post',
13 | closeBundle() {
14 | const outDir = 'dist';
15 | const indexPath = path.join(outDir, 'index.html');
16 | if (fs.existsSync(indexPath)) {
17 | let html = fs.readFileSync(indexPath, 'utf-8');
18 | if (html.includes(METRIKA_COMMENT)) {
19 | html = html.replace(METRIKA_COMMENT, METRIKA_SNIPPET);
20 | fs.writeFileSync(indexPath, html, 'utf-8');
21 | }
22 | }
23 | },
24 | };
25 | }
26 |
--------------------------------------------------------------------------------
/src/services/Base64ConverterService.ts:
--------------------------------------------------------------------------------
1 | export interface Base64FileModel {
2 | file: File;
3 | name: string;
4 | mimeType: string;
5 | dataUrl: string;
6 | originalSize: number; // in bytes
7 | base64Size: number; // in bytes
8 | }
9 |
10 | import { injectable, ServiceLifetime } from "fw";
11 |
12 | @injectable(ServiceLifetime.Singleton)
13 | export class Base64ConverterService {
14 | async convertFilesToBase64(files: File[], onProgress?: (progress: number) => void): Promise {
15 | const results: Base64FileModel[] = [];
16 | for (let i = 0; i < files.length; i++) {
17 | const file = files[i];
18 | const mimeType = file.type || 'application/octet-stream';
19 | const dataUrl = await this.fileToDataUrl(file);
20 | // Calculate base64 size (actual base64 string length in bytes)
21 | const base64String = dataUrl.split(',')[1] || '';
22 | results.push({
23 | file,
24 | name: file.name,
25 | mimeType,
26 | dataUrl,
27 | originalSize: file.size,
28 | base64Size: base64String.length,
29 | });
30 | if (onProgress) {
31 | onProgress(Math.round(((i + 1) / files.length) * 100));
32 | }
33 | }
34 | return results;
35 | }
36 |
37 | private fileToDataUrl(file: File): Promise {
38 | return new Promise((resolve, reject) => {
39 | const reader = new FileReader();
40 | reader.onload = () => resolve(reader.result as string);
41 | reader.onerror = reject;
42 | reader.readAsDataURL(file);
43 | });
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/test-version-fetch.js:
--------------------------------------------------------------------------------
1 | // Manual test script for version checking
2 | // Run this in the browser console to test version fetching
3 |
4 | async function testVersionFetching() {
5 | console.log('🧪 Testing version fetching...');
6 |
7 | // Test different URL patterns that might cause 304 responses
8 | const testUrls = [
9 | './version.json',
10 | './version.json?t=' + Date.now(),
11 | '/version.json?cb=' + Math.random()
12 | ];
13 |
14 | for (const url of testUrls) {
15 | try {
16 | console.log(`🔍 Testing URL: ${url}`);
17 |
18 | const response = await fetch(url, {
19 | method: 'GET',
20 | headers: {
21 | 'Cache-Control': 'no-cache, no-store, must-revalidate',
22 | 'Pragma': 'no-cache',
23 | 'Expires': '0'
24 | },
25 | cache: 'no-store'
26 | });
27 |
28 | console.log(`Status: ${response.status} ${response.statusText}`);
29 | console.log(`Cache-Control: ${response.headers.get('cache-control')}`);
30 |
31 | if (response.status === 200 || response.status === 304) {
32 | try {
33 | const data = await response.json();
34 | console.log('✅ Success:', data);
35 | } catch (jsonError) {
36 | console.log('⚠️ Could not parse JSON (possibly 304 with no body)');
37 | }
38 | } else {
39 | console.log('❌ Failed:', response.status);
40 | }
41 |
42 | console.log('---');
43 |
44 | } catch (error) {
45 | console.error('❌ Request failed:', error);
46 | }
47 | }
48 | }
49 |
50 | // Run the test
51 | testVersionFetching();
52 |
--------------------------------------------------------------------------------
/src/services/PreviewServiceValidators/GeneralValidator.ts:
--------------------------------------------------------------------------------
1 | import type { Validator, ValidationResult, ValidationCategory } from './types';
2 |
3 | export class GeneralValidator implements Validator {
4 | validate(content: string, _fileSize: number): ValidationResult {
5 | const categories: ValidationCategory[] = [
6 | {
7 | name: 'General',
8 | checks: [
9 | {
10 | name: 'Not using "window.top" access',
11 | passed: !content.includes('window.top'),
12 | details: content.includes('window.top')
13 | ? 'Playable contains window.top access which may cause issues in ad environments'
14 | : undefined
15 | },
16 | {
17 | name: 'No external script loading (except mraid.js, exitapi.js)',
18 | passed: !/
50 |