nanoplayer 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,32 @@
1
+ # NanoPlayer 🎬
2
+
3
+ **NanoPlayer** — это лёгкий, dependency-free HTML5 видеоплеер на чистом JavaScript
4
+ с интерфейсом в стиле YouTube.
5
+
6
+ Проект создан как **open-source** и может использоваться как в личных,
7
+ так и в коммерческих проектах.
8
+
9
+ ---
10
+
11
+ ## ✨ Возможности
12
+
13
+ - ▶️ Play / Pause (кнопка, пробел)
14
+ - 🎞 Превью видео (poster или первый кадр автоматически)
15
+ - ⏱ Прогресс и перемотка
16
+ - 🔊 Громкость (выпадающее меню)
17
+ - ⚙️ Скорость воспроизведения
18
+ - ℹ️ Информация о плеере
19
+ - ⛶ Полноэкранный режим
20
+ - ⌨️ Управление с клавиатуры
21
+ - 🎨 Кастомный UI (без native controls)
22
+ - ❌ Без зависимостей
23
+
24
+ ---
25
+
26
+ ## 📦 Установка
27
+
28
+ ### Через npm (рекомендуется)
29
+
30
+ ```bash
31
+ npm install nanoplayer
32
+ ```
package/README.md ADDED
@@ -0,0 +1,128 @@
1
+ # NanoPlayer 🎬
2
+
3
+ NanoPlayer — лёгкий, dependency-free HTML5 видеоплеер на чистом JavaScript
4
+ с интерфейсом в стиле YouTube.
5
+
6
+ Проект создаётся как open-source и подходит как для личных,
7
+ так и для коммерческих проектов.
8
+
9
+ ---
10
+
11
+ ## Возможности
12
+
13
+ - Play / Pause (кнопка и пробел)
14
+ - Превью видео (poster или первый кадр автоматически)
15
+ - Прогресс и перемотка
16
+ - Громкость (выпадающее меню)
17
+ - Скорость воспроизведения
18
+ - Информация о плеере
19
+ - Полноэкранный режим
20
+ - Управление с клавиатуры
21
+ - Кастомный UI (без native controls)
22
+ - Без зависимостей
23
+
24
+ ---
25
+
26
+ ## Установка
27
+
28
+ Через npm:
29
+ ```
30
+ npm install nanoplayer
31
+ ```
32
+ ---
33
+
34
+ ## Быстрый старт
35
+
36
+ HTML (UMD):
37
+ ```
38
+ <link rel="stylesheet" href="dist/style.css">
39
+
40
+ <div id="player"></div>
41
+
42
+ <script src="dist/nanoplayer.umd.js"></script>
43
+ <script>
44
+ new NanoPlayer('#player', {
45
+ src: 'video.mp4',
46
+ poster: 'poster.jpg'
47
+ })
48
+ </script>
49
+ ```
50
+ ---
51
+
52
+ ES Modules:
53
+ ```
54
+ import NanoPlayer from 'nanoplayer'
55
+ import 'nanoplayer/dist/style.css'
56
+
57
+ new NanoPlayer('#player', {
58
+ src: 'video.mp4'
59
+ })
60
+ ```
61
+ Важно: ES-модули должны запускаться через HTTP-сервер, не file://
62
+
63
+ ---
64
+
65
+ ## Параметры
66
+
67
+ new NanoPlayer(selector, options)
68
+
69
+ options:
70
+
71
+ - src (string) — путь к видео
72
+ - poster (string | null) — превью (если не указано, берётся первый кадр)
73
+ - autoplay (boolean) — автозапуск
74
+ - volume (number) — громкость (0–1)
75
+ - playbackRates (number[]) — доступные скорости
76
+
77
+ ---
78
+
79
+ ## Управление с клавиатуры
80
+
81
+ - Space — Play / Pause
82
+ - Esc — выход из fullscreen
83
+
84
+ Пробел не перехватывается, если фокус находится в input, textarea или select.
85
+
86
+ ---
87
+
88
+ ## Стилизация
89
+
90
+ NanoPlayer не влияет на глобальные стили страницы.
91
+ Все CSS-классы имеют префикс nano-.
92
+
93
+ Можно:
94
+ - переопределять стили
95
+ - писать собственные темы
96
+ - полностью заменить style.css
97
+
98
+ ---
99
+
100
+ ## Архитектура
101
+
102
+ .nano-player
103
+ ├── video
104
+ ├── overlay (big play + info)
105
+ ├── controls
106
+ │ ├── left (play)
107
+ │ ├── center (progress)
108
+ │ └── right (menus)
109
+ │ ├── volume
110
+ │ ├── settings
111
+ │ └── info
112
+
113
+ Каждое меню — отдельный popover, как в YouTube.
114
+
115
+ ---
116
+
117
+ ## Структура проекта
118
+
119
+ nanoplayer/
120
+ ├── src/
121
+ │ ├── NanoPlayer.js
122
+ │ └── index.js
123
+ │ └── style.css
124
+ ├── dist/
125
+ │ ├── nanoplayer.umd.js
126
+ │ └── nanoplayer.es.js
127
+ │ └── nanoplayer.umd.js
128
+
@@ -0,0 +1 @@
1
+ .nano-player{position:relative;max-width:720px;aspect-ratio:16 / 9;background:#000;border-radius:12px;overflow:hidden;font-family:system-ui,sans-serif}.nano-player video{width:100%;height:100%;display:block}.nano-overlay{position:absolute;inset:0;background:#0006;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px}.nano-overlay.hidden{display:none}.nano-big-play{width:80px;height:80px;border-radius:50%;border:none;font-size:32px;cursor:pointer}.nano-info-overlay{color:#fff;font-size:14px;text-align:center}.nano-controls{position:absolute;bottom:0;left:0;right:0;display:grid;grid-template-columns:auto 1fr auto;gap:10px;padding:8px;background:linear-gradient(to top,rgba(0,0,0,.7),transparent)}.nano-left,.nano-center,.nano-right{display:flex;align-items:center;gap:6px}.nano-icon{background:none;border:none;color:#fff;font-size:16px;cursor:pointer}.nano-progress{width:100%;height:6px;background:#ffffff4d;cursor:pointer}.nano-progress-fill{height:100%;background:#fff;width:0%}.nano-menu-wrap{position:relative}.nano-menu{position:absolute;bottom:36px;right:0;min-width:140px;background:#000000e6;border-radius:6px;padding:8px;display:none;color:#fff;font-size:13px}.nano-menu-wrap.open .nano-menu{display:block}.nano-menu button{width:100%;background:none;border:none;color:#fff;padding:6px;cursor:pointer;text-align:left}.nano-menu button:hover{background:#ffffff1a}.nano-menu input[type=range]{width:100%}.nano-player.nano-fullscreen{width:100vw;height:100vh;max-width:none;border-radius:0}
@@ -0,0 +1,95 @@
1
+ class l {
2
+ constructor(e, t = {}) {
3
+ if (this.container = document.querySelector(e), !this.container)
4
+ throw new Error("NanoPlayer: container not found");
5
+ this.options = {
6
+ src: "",
7
+ poster: null,
8
+ autoplay: !1,
9
+ volume: 1,
10
+ playbackRates: [0.5, 1, 1.5, 2],
11
+ ...t
12
+ }, this.init();
13
+ }
14
+ /* ================= INIT ================= */
15
+ init() {
16
+ this.wrapper = document.createElement("div"), this.wrapper.className = "nano-player", this.wrapper.tabIndex = 0, this.video = document.createElement("video"), this.video.src = this.options.src, this.video.autoplay = this.options.autoplay, this.video.volume = this.options.volume, this.video.preload = "metadata", this.video.controls = !1, this.options.poster && (this.video.poster = this.options.poster), this.wrapper.appendChild(this.video), this.container.appendChild(this.wrapper), this.createOverlay(), this.createControls(), this.bindEvents();
17
+ }
18
+ /* ================= OVERLAY ================= */
19
+ createOverlay() {
20
+ this.overlay = document.createElement("div"), this.overlay.className = "nano-overlay", this.bigPlay = document.createElement("button"), this.bigPlay.className = "nano-big-play", this.bigPlay.textContent = "▶", this.overlay.append(this.bigPlay, this.infoOverlay), this.wrapper.appendChild(this.overlay), this.bigPlay.onclick = () => this.video.play();
21
+ }
22
+ /* ================= CONTROLS ================= */
23
+ createControls() {
24
+ this.controls = document.createElement("div"), this.controls.className = "nano-controls", this.left = document.createElement("div"), this.left.className = "nano-left", this.playBtn = document.createElement("button"), this.playBtn.className = "nano-icon", this.playBtn.textContent = "▶", this.left.appendChild(this.playBtn), this.center = document.createElement("div"), this.center.className = "nano-center", this.progress = document.createElement("div"), this.progress.className = "nano-progress", this.progressFill = document.createElement("div"), this.progressFill.className = "nano-progress-fill", this.progress.appendChild(this.progressFill), this.center.appendChild(this.progress), this.right = document.createElement("div"), this.right.className = "nano-right", this.volumeMenu = this.createMenu("🔊"), this.volumeSlider = document.createElement("input"), this.volumeSlider.type = "range", this.volumeSlider.min = 0, this.volumeSlider.max = 1, this.volumeSlider.step = 0.01, this.volumeSlider.value = this.video.volume, this.volumeMenu.menu.appendChild(this.volumeSlider), this.settingsMenu = this.createMenu("⚙️"), this.options.playbackRates.forEach((e) => {
25
+ const t = document.createElement("button");
26
+ t.textContent = `${e}x`, t.onclick = () => {
27
+ this.video.playbackRate = e, this.settingsMenu.close();
28
+ }, this.settingsMenu.menu.appendChild(t);
29
+ }), this.infoMenu = this.createMenu("ℹ️"), this.infoMenu.menu.innerHTML = `
30
+ <strong>NanoPlayer</strong><br>
31
+ Version 0.1.0<br>
32
+ MIT License
33
+ swiftmessage.org
34
+ github.com/swiftmessage/NanoPlayer
35
+ `, this.fullscreenBtn = document.createElement("button"), this.fullscreenBtn.className = "nano-icon", this.fullscreenBtn.textContent = "⛶", this.fullscreenBtn.onclick = () => this.toggleFullscreen(), this.right.append(
36
+ this.volumeMenu.root,
37
+ this.settingsMenu.root,
38
+ this.infoMenu.root,
39
+ this.fullscreenBtn
40
+ ), this.controls.append(this.left, this.center, this.right), this.wrapper.appendChild(this.controls), this.playBtn.onclick = () => this.video.paused ? this.video.play() : this.video.pause(), this.progress.onclick = (e) => {
41
+ const t = this.progress.getBoundingClientRect();
42
+ this.video.currentTime = (e.clientX - t.left) / t.width * this.video.duration;
43
+ }, this.volumeSlider.oninput = () => this.video.volume = this.volumeSlider.value;
44
+ }
45
+ /* ================= MENU HELPER ================= */
46
+ createMenu(e) {
47
+ const t = document.createElement("div");
48
+ t.className = "nano-menu-wrap";
49
+ const s = document.createElement("button");
50
+ s.className = "nano-icon", s.textContent = e;
51
+ const i = document.createElement("div");
52
+ return i.className = "nano-menu", s.onclick = (n) => {
53
+ n.stopPropagation(), document.querySelectorAll(".nano-menu-wrap.open").forEach((o) => o.classList.remove("open")), t.classList.toggle("open");
54
+ }, document.addEventListener("click", () => {
55
+ t.classList.remove("open");
56
+ }), t.append(s, i), {
57
+ root: t,
58
+ menu: i,
59
+ close: () => t.classList.remove("open")
60
+ };
61
+ }
62
+ /* ================= EVENTS ================= */
63
+ bindEvents() {
64
+ this.wrapper.addEventListener("click", () => {
65
+ this.wrapper.focus();
66
+ }), this.video.addEventListener("loadedmetadata", () => {
67
+ this.options.poster || (this.video.currentTime = 0.1, this.video.pause());
68
+ }), this.video.addEventListener("play", () => {
69
+ this.playBtn.textContent = "❚❚", this.overlay.classList.add("hidden");
70
+ }), this.video.addEventListener("pause", () => {
71
+ this.playBtn.textContent = "▶";
72
+ }), this.video.addEventListener("timeupdate", () => {
73
+ const e = this.video.currentTime / this.video.duration * 100;
74
+ this.progressFill.style.width = `${e}%`;
75
+ }), this.wrapper.addEventListener("keydown", (e) => {
76
+ if (e.code !== "Space") return;
77
+ const t = e.target.tagName;
78
+ ["INPUT", "TEXTAREA", "SELECT"].includes(t) || (e.preventDefault(), this.video.paused ? this.video.play() : this.video.pause());
79
+ }), document.addEventListener("fullscreenchange", () => {
80
+ this.wrapper.classList.toggle(
81
+ "nano-fullscreen",
82
+ !!document.fullscreenElement
83
+ );
84
+ });
85
+ }
86
+ /* ================= FULLSCREEN ================= */
87
+ toggleFullscreen() {
88
+ const e = this.wrapper;
89
+ document.fullscreenElement ? document.exitFullscreen ? document.exitFullscreen() : document.webkitExitFullscreen ? document.webkitExitFullscreen() : document.msExitFullscreen && document.msExitFullscreen() : e.requestFullscreen ? e.requestFullscreen() : e.webkitRequestFullscreen ? e.webkitRequestFullscreen() : e.msRequestFullscreen && e.msRequestFullscreen();
90
+ }
91
+ }
92
+ typeof window < "u" && (window.NanoPlayer = l);
93
+ export {
94
+ l as default
95
+ };
@@ -0,0 +1,7 @@
1
+ (function(s,i){typeof exports=="object"&&typeof module<"u"?module.exports=i():typeof define=="function"&&define.amd?define(i):(s=typeof globalThis<"u"?globalThis:s||self,s.NanoPlayer=i())})(this,(function(){"use strict";class s{constructor(e,t={}){if(this.container=document.querySelector(e),!this.container)throw new Error("NanoPlayer: container not found");this.options={src:"",poster:null,autoplay:!1,volume:1,playbackRates:[.5,1,1.5,2],...t},this.init()}init(){this.wrapper=document.createElement("div"),this.wrapper.className="nano-player",this.wrapper.tabIndex=0,this.video=document.createElement("video"),this.video.src=this.options.src,this.video.autoplay=this.options.autoplay,this.video.volume=this.options.volume,this.video.preload="metadata",this.video.controls=!1,this.options.poster&&(this.video.poster=this.options.poster),this.wrapper.appendChild(this.video),this.container.appendChild(this.wrapper),this.createOverlay(),this.createControls(),this.bindEvents()}createOverlay(){this.overlay=document.createElement("div"),this.overlay.className="nano-overlay",this.bigPlay=document.createElement("button"),this.bigPlay.className="nano-big-play",this.bigPlay.textContent="▶",this.overlay.append(this.bigPlay,this.infoOverlay),this.wrapper.appendChild(this.overlay),this.bigPlay.onclick=()=>this.video.play()}createControls(){this.controls=document.createElement("div"),this.controls.className="nano-controls",this.left=document.createElement("div"),this.left.className="nano-left",this.playBtn=document.createElement("button"),this.playBtn.className="nano-icon",this.playBtn.textContent="▶",this.left.appendChild(this.playBtn),this.center=document.createElement("div"),this.center.className="nano-center",this.progress=document.createElement("div"),this.progress.className="nano-progress",this.progressFill=document.createElement("div"),this.progressFill.className="nano-progress-fill",this.progress.appendChild(this.progressFill),this.center.appendChild(this.progress),this.right=document.createElement("div"),this.right.className="nano-right",this.volumeMenu=this.createMenu("🔊"),this.volumeSlider=document.createElement("input"),this.volumeSlider.type="range",this.volumeSlider.min=0,this.volumeSlider.max=1,this.volumeSlider.step=.01,this.volumeSlider.value=this.video.volume,this.volumeMenu.menu.appendChild(this.volumeSlider),this.settingsMenu=this.createMenu("⚙️"),this.options.playbackRates.forEach(e=>{const t=document.createElement("button");t.textContent=`${e}x`,t.onclick=()=>{this.video.playbackRate=e,this.settingsMenu.close()},this.settingsMenu.menu.appendChild(t)}),this.infoMenu=this.createMenu("ℹ️"),this.infoMenu.menu.innerHTML=`
2
+ <strong>NanoPlayer</strong><br>
3
+ Version 0.1.0<br>
4
+ MIT License
5
+ swiftmessage.org
6
+ github.com/swiftmessage/NanoPlayer
7
+ `,this.fullscreenBtn=document.createElement("button"),this.fullscreenBtn.className="nano-icon",this.fullscreenBtn.textContent="⛶",this.fullscreenBtn.onclick=()=>this.toggleFullscreen(),this.right.append(this.volumeMenu.root,this.settingsMenu.root,this.infoMenu.root,this.fullscreenBtn),this.controls.append(this.left,this.center,this.right),this.wrapper.appendChild(this.controls),this.playBtn.onclick=()=>this.video.paused?this.video.play():this.video.pause(),this.progress.onclick=e=>{const t=this.progress.getBoundingClientRect();this.video.currentTime=(e.clientX-t.left)/t.width*this.video.duration},this.volumeSlider.oninput=()=>this.video.volume=this.volumeSlider.value}createMenu(e){const t=document.createElement("div");t.className="nano-menu-wrap";const n=document.createElement("button");n.className="nano-icon",n.textContent=e;const o=document.createElement("div");return o.className="nano-menu",n.onclick=l=>{l.stopPropagation(),document.querySelectorAll(".nano-menu-wrap.open").forEach(r=>r.classList.remove("open")),t.classList.toggle("open")},document.addEventListener("click",()=>{t.classList.remove("open")}),t.append(n,o),{root:t,menu:o,close:()=>t.classList.remove("open")}}bindEvents(){this.wrapper.addEventListener("click",()=>{this.wrapper.focus()}),this.video.addEventListener("loadedmetadata",()=>{this.options.poster||(this.video.currentTime=.1,this.video.pause())}),this.video.addEventListener("play",()=>{this.playBtn.textContent="❚❚",this.overlay.classList.add("hidden")}),this.video.addEventListener("pause",()=>{this.playBtn.textContent="▶"}),this.video.addEventListener("timeupdate",()=>{const e=this.video.currentTime/this.video.duration*100;this.progressFill.style.width=`${e}%`}),this.wrapper.addEventListener("keydown",e=>{if(e.code!=="Space")return;const t=e.target.tagName;["INPUT","TEXTAREA","SELECT"].includes(t)||(e.preventDefault(),this.video.paused?this.video.play():this.video.pause())}),document.addEventListener("fullscreenchange",()=>{this.wrapper.classList.toggle("nano-fullscreen",!!document.fullscreenElement)})}toggleFullscreen(){const e=this.wrapper;document.fullscreenElement?document.exitFullscreen?document.exitFullscreen():document.webkitExitFullscreen?document.webkitExitFullscreen():document.msExitFullscreen&&document.msExitFullscreen():e.requestFullscreen?e.requestFullscreen():e.webkitRequestFullscreen?e.webkitRequestFullscreen():e.msRequestFullscreen&&e.msRequestFullscreen()}}return typeof window<"u"&&(window.NanoPlayer=s),s}));
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "nanoplayer",
3
+ "version": "0.1.0",
4
+ "description": "NanoPlayer — lightweight vanilla JavaScript video player",
5
+ "main": "dist/nanoplayer.umd.js",
6
+ "module": "dist/nanoplayer.es.js",
7
+ "files": [
8
+ "dist"
9
+ ],
10
+ "scripts": {
11
+ "build": "vite build"
12
+ },
13
+ "keywords": [
14
+ "video",
15
+ "player",
16
+ "html5",
17
+ "vanilla-js",
18
+ "nano"
19
+ ],
20
+ "author": "SWIFTMESSAGE",
21
+ "license": "MIT",
22
+ "devDependencies": {
23
+ "vite": "^7.3.1"
24
+ }
25
+ }