Pause Point

Creates a pause screen before you enter websites with breathing exercises and alternative activities.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         Pause Point
// @namespace    http://tampermonkey.net/
// @version      1.0.0
// @description  Creates a pause screen before you enter websites with breathing exercises and alternative activities.
// @author       KHROTU
// @match        https://www.youtube.com/*
// @match        https://www.reddit.com/*
// @match        https://twitter.com/*
// @match        https://x.com/*
// @match        https://www.instagram.com/*
// @match        https://www.tiktok.com/*
// @grant        none
// @run-at       document-start
// @noframes
// @license      MIT
// ==/UserScript==

(function() {
	'use strict';
	const USER_CONFIG = {
		customizations: [
			// {
			//     url: 'https://youtube.com',
			//     matchType: 'domain',    // 'domain' | 'exact' | 'contains'
			//     altUrls: [
			//         'https://en.wikipedia.org',
			//         'https://www.google.com',
			//         'https://news.ycombinator.com'
			//     ],
			//     altActivities: [
			//         'Read 10 pages of a book',
			//         'Do 20 push-ups',
			//         'Write down 3 things you are grateful for',
			//         'Organize your desk for 5 minutes'
			//     ],
			//     nob: 3,    // number of breaths
			//     customMessage: 'Consider these though',
			//     enabled: true
			// },
			// {
			//     url: 'https://x.com',
			//     matchType: 'domain',
			//     altUrls: [
			//         'https://github.com',
			//         'https://www.duolingo.com',
			//         'https://www.coursera.org'
			//     ],
			//     altActivities: [
			//         'Work on a personal project for 15 minutes',
			//         'Meditate for 5 minutes',
			//         'Review your goals for the week'
			//     ],
			//     nob: 2,
			//     enabled: true
			// }
		],
		defaultSettings: {
			dnob: 3,
			dMessage: 'but do you really need to?'
		}
	};
	const INHALE_MS = 2000;
	const HOLD_IN_MS = 500;
	const EXHALE_MS = 2000;
	const HOLD_OUT_MS = 500;
	const BREATH_CYCLE_MS = INHALE_MS + HOLD_IN_MS + EXHALE_MS + HOLD_OUT_MS;
	const BYPASS_KEY = 'pp_bypass';
	const BYPASS_DURATION_MS = 5 * 60 * 1000;
	function getConfig() {
		const currentUrl = location.href;
		const currentHost = location.hostname.replace(/^www\./, '');
		let match = null;
		for (const c of USER_CONFIG.customizations) {
			if (!c.enabled) continue;
			const testUrl = c.url.replace(/^www\./, '');
			const testHost = new URL(testUrl).hostname.replace(/^www\./, '');
			let matched = false;
			if (c.matchType === 'exact') {
				matched = currentUrl === c.url;
			} else if (c.matchType === 'contains') {
				matched = currentUrl.includes(testUrl);
			} else {
				matched = currentHost === testHost || currentHost.endsWith('.' + testHost);
			}
			if (matched) { match = c; break; }
		}
		const defaults = USER_CONFIG.defaultSettings || {};
		return {
			nob: match?.nob ?? defaults.dnob ?? 3,
			message: match?.customMessage ?? defaults.dMessage ?? 'but do you really need to?',
			altUrls: match?.altUrls ?? [],
			altActivities: match?.altActivities ?? [
				'Take a short walk',
				'Drink a glass of water',
				'Write for five minutes',
				'Stretch your body',
				'Read a book chapter',
				'Tidy your workspace'
			]
		};
	}
	const bypass = sessionStorage.getItem(BYPASS_KEY);
	if (bypass && Date.now() - parseInt(bypass, 10) < BYPASS_DURATION_MS) return;
	const cfg = getConfig();
	const hostname = location.hostname.replace(/^www\./, '');
	const faviconUrl = 'https://www.google.com/s2/favicons?sz=64&domain=' + location.hostname;
	const totalBreathMs = cfg.nob * BREATH_CYCLE_MS;
	document.documentElement.style.setProperty('display', 'none', 'important');
	const takeover = () => {
		if (!document.body) {
			requestAnimationFrame(takeover);
			return;
		}
		window.stop();
		const head = document.head;
		const body = document.body;
		while (head.firstChild) head.removeChild(head.firstChild);
		while (body.firstChild) body.removeChild(body.firstChild);
		head.appendChild(Object.assign(document.createElement('title'), {
			textContent: 'Pause Point'
		}));
		const fontLink = document.createElement('link');
		fontLink.rel = 'stylesheet';
		fontLink.href = 'https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,600;1,400&family=Inter:wght@300;400;500&display=swap';
		head.appendChild(fontLink);
		const style = document.createElement('style');
		style.textContent = `
			body {
				background: #09090b;
				color: #e4e4e7;
				font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
				margin: 0;
				height: 100vh;
				overflow: hidden;
				display: flex;
				align-items: center;
				justify-content: center;
			}
			.phase {
				position: absolute;
				inset: 0;
				display: flex;
				flex-direction: column;
				align-items: center;
				justify-content: center;
				transition: opacity 0.8s ease;
			}
			.fluid-wrap {
				position: relative;
				width: 420px;
				height: 420px;
				display: flex;
				align-items: center;
				justify-content: center;
			}
			.fluid-blob {
				position: absolute;
				filter: blur(60px);
				will-change: transform, opacity;
			}
			.fluid-blob:nth-child(1) {
				width: 260px;
				height: 260px;
				background: radial-gradient(circle at 30% 30%, rgba(150, 170, 205, 0.38), rgba(120, 140, 180, 0.14));
				border-radius: 60% 40% 55% 45% / 55% 45% 60% 40%;
				animation: blobDrift1 14s ease-in-out infinite alternate;
			}
			.fluid-blob:nth-child(2) {
				width: 320px;
				height: 320px;
				background: radial-gradient(circle at 40% 60%, rgba(180, 160, 200, 0.32), rgba(150, 130, 180, 0.12));
				border-radius: 45% 55% 40% 60% / 50% 60% 45% 55%;
				animation: blobDrift2 18s ease-in-out infinite alternate;
			}
			.fluid-blob:nth-child(3) {
				width: 220px;
				height: 220px;
				background: radial-gradient(circle at 70% 30%, rgba(160, 195, 180, 0.34), rgba(130, 170, 150, 0.13));
				border-radius: 55% 45% 60% 40% / 45% 55% 40% 60%;
				animation: blobDrift3 16s ease-in-out infinite alternate;
			}
			.fluid-blob:nth-child(4) {
				width: 280px;
				height: 280px;
				background: radial-gradient(circle at 50% 50%, rgba(195, 170, 175, 0.28), rgba(170, 145, 150, 0.1));
				border-radius: 50% 50% 45% 55% / 55% 45% 50% 50%;
				animation: blobDrift4 20s ease-in-out infinite alternate;
			}
			.fluid-blob:nth-child(5) {
				width: 200px;
				height: 200px;
				background: radial-gradient(circle at 60% 40%, rgba(165, 185, 205, 0.26), rgba(140, 160, 185, 0.09));
				border-radius: 48% 52% 58% 42% / 42% 48% 52% 58%;
				animation: blobDrift5 12s ease-in-out infinite alternate;
			}
			@keyframes blobDrift1 { 0% { transform: translate(-10%, -10%) rotate(0deg); } 100% { transform: translate(10%, 10%) rotate(20deg); } }
			@keyframes blobDrift2 { 0% { transform: translate(10%, -5%) rotate(0deg); } 100% { transform: translate(-10%, 5%) rotate(-15deg); } }
			@keyframes blobDrift3 { 0% { transform: translate(-5%, 10%) rotate(0deg); } 100% { transform: translate(5%, -10%) rotate(25deg); } }
			@keyframes blobDrift4 { 0% { transform: translate(5%, 5%) rotate(0deg); } 100% { transform: translate(-5%, -5%) rotate(-20deg); } }
			@keyframes blobDrift5 { 0% { transform: translate(0%, -10%) rotate(0deg); } 100% { transform: translate(0%, 10%) rotate(30deg); } }
			.breath-ring {
				position: absolute;
				width: 180px;
				height: 180px;
				border-radius: 50%;
				border: 1.5px solid rgba(220, 225, 235, 0.1);
				animation: ringPulse var(--cycle) ease-in-out infinite;
			}
			.breath-ring:nth-of-type(2) { animation-delay: calc(var(--cycle) * 0.08); width: 220px; height: 220px; }
			.breath-ring:nth-of-type(3) { animation-delay: calc(var(--cycle) * 0.16); width: 260px; height: 260px; }
			.breath-ring:nth-of-type(4) { animation-delay: calc(var(--cycle) * 0.24); width: 300px; height: 300px; }
			@keyframes ringPulse {
				0%, 100% { transform: scale(0.72); opacity: 0.1; }
				40%, 50% { transform: scale(1.18); opacity: 0.28; }
			}
			.core-orb {
				position: absolute;
				width: 140px;
				height: 140px;
				border-radius: 50%;
				background: radial-gradient(circle at 35% 35%, rgba(210, 220, 240, 0.35), rgba(190, 200, 225, 0.12) 55%, transparent 72%);
				animation: orbBreathe var(--cycle) ease-in-out infinite;
				box-shadow: 0 0 80px rgba(180, 195, 230, 0.18), inset 0 0 50px rgba(210, 220, 240, 0.12);
			}
			@keyframes orbBreathe {
				0%, 100% { transform: scale(0.82); opacity: 0.55; }
				40% { transform: scale(1.18); opacity: 1; }
				50% { transform: scale(1.18); opacity: 1; }
				90% { transform: scale(0.82); opacity: 0.55; }
			}
			.breath-label {
				position: absolute;
				font-size: 13px;
				letter-spacing: 4px;
				text-transform: lowercase;
				color: rgba(255, 255, 255, 0.55);
				transition: opacity 0.5s ease;
				pointer-events: none;
				font-weight: 400;
			}
			.breath-counter {
				position: absolute;
				bottom: -52px;
				font-size: 11px;
				letter-spacing: 3px;
				color: rgba(255, 255, 255, 0.3);
				transition: opacity 0.4s ease;
			}
			.prompt-wrap {
				opacity: 0;
				display: none;
				flex-direction: column;
				align-items: center;
				justify-content: center;
				text-align: center;
				padding: 24px;
				max-width: 640px;
				width: 100%;
			}
			.prompt-header {
				display: flex;
				align-items: center;
				gap: 14px;
				flex-wrap: wrap;
				justify-content: center;
				margin-bottom: 8px;
			}
			.prompt-favicon {
				width: 36px;
				height: 36px;
				border-radius: 8px;
				background: rgba(255, 255, 255, 0.05);
				padding: 5px;
				box-sizing: border-box;
			}
			h1 {
				font-family: 'Playfair Display', Georgia, 'Times New Roman', serif;
				font-size: 34px;
				font-weight: 400;
				margin: 0;
				color: #f0f0f0;
				letter-spacing: -0.3px;
				line-height: 1.2;
			}
			.subtext {
				font-family: 'Playfair Display', Georgia, 'Times New Roman', serif;
				font-size: 20px;
				font-style: italic;
				color: #71717a;
				margin: 6px 0 44px;
			}
			.alts-label {
				font-size: 11px;
				text-transform: lowercase;
				letter-spacing: 2px;
				color: #3f3f46;
				margin-bottom: 16px;
			}
			.alts-grid {
				display: grid;
				grid-template-columns: 1fr;
				gap: 10px;
				width: 100%;
				max-width: 460px;
				list-style: none;
				padding: 0;
				margin: 0;
			}
			.alt-item {
				background: rgba(255, 255, 255, 0.03);
				border: 1px solid rgba(255, 255, 255, 0.05);
				border-radius: 12px;
				padding: 14px 18px;
				font-size: 14px;
				color: #a1a1aa;
				text-decoration: none;
				transition: background 0.2s ease, border-color 0.2s ease, transform 0.2s ease;
				cursor: default;
				display: flex;
				align-items: center;
				justify-content: space-between;
			}
			.alt-item[href] {
				cursor: pointer;
			}
			.alt-item[href]:hover {
				background: rgba(255, 255, 255, 0.07);
				border-color: rgba(255, 255, 255, 0.12);
				transform: translateY(-1px);
			}
			.alt-arrow {
				opacity: 0;
				transition: opacity 0.2s ease;
				font-size: 15px;
				color: #71717a;
			}
			.alt-item[href]:hover .alt-arrow {
				opacity: 1;
			}
			.continue-btn {
				margin-top: 32px;
				font-size: 12px;
				color: #3f3f46;
				text-decoration: underline;
				text-underline-offset: 3px;
				cursor: pointer;
				background: none;
				border: none;
				padding: 8px;
				transition: color 0.2s ease;
				font-family: inherit;
			}
			.continue-btn:hover {
				color: #71717a;
			}
		`;
		head.appendChild(style);
		const breathPhase = document.createElement('div');
		breathPhase.className = 'phase';
		breathPhase.id = 'pp-breath';
		const fluidWrap = document.createElement('div');
		fluidWrap.className = 'fluid-wrap';
		fluidWrap.style.setProperty('--cycle', BREATH_CYCLE_MS + 'ms');
		for (let i = 0; i < 5; i++) {
			const blob = document.createElement('div');
			blob.className = 'fluid-blob';
			fluidWrap.appendChild(blob);
		}
		for (let i = 0; i < 4; i++) {
			const ring = document.createElement('div');
			ring.className = 'breath-ring';
			fluidWrap.appendChild(ring);
		}
		const core = document.createElement('div');
		core.className = 'core-orb';
		fluidWrap.appendChild(core);
		const breathLabel = document.createElement('div');
		breathLabel.className = 'breath-label';
		breathLabel.textContent = 'breathe in';
		fluidWrap.appendChild(breathLabel);
		const breathCounter = document.createElement('div');
		breathCounter.className = 'breath-counter';
		breathCounter.textContent = '1 / ' + cfg.nob;
		fluidWrap.appendChild(breathCounter);
		breathPhase.appendChild(fluidWrap);
		body.appendChild(breathPhase);
		const promptPhase = document.createElement('div');
		promptPhase.className = 'phase prompt-wrap';
		promptPhase.id = 'pp-prompt';
		const header = document.createElement('div');
		header.className = 'prompt-header';
		const faviconImg = document.createElement('img');
		faviconImg.className = 'prompt-favicon';
		faviconImg.src = faviconUrl;
		faviconImg.alt = '';
		header.appendChild(faviconImg);
		const h1 = document.createElement('h1');
		h1.textContent = 'You are about to visit ' + hostname;
		header.appendChild(h1);
		promptPhase.appendChild(header);
		const sub = document.createElement('p');
		sub.className = 'subtext';
		sub.textContent = cfg.message;
		promptPhase.appendChild(sub);
		if (cfg.altUrls.length > 0 || cfg.altActivities.length > 0) {
			const label = document.createElement('div');
			label.className = 'alts-label';
			label.textContent = 'instead, consider:';
			promptPhase.appendChild(label);
			const grid = document.createElement('ul');
			grid.className = 'alts-grid';
			for (const url of cfg.altUrls) {
				let name;
				try { name = new URL(url).hostname.replace(/^www\./, ''); }
				catch { name = url; }
				const li = document.createElement('li');
				const item = document.createElement('a');
				item.className = 'alt-item';
				item.href = url;
				item.target = '_blank';
				item.rel = 'noopener noreferrer';
				item.appendChild(document.createTextNode(name));
				const arrow = document.createElement('span');
				arrow.className = 'alt-arrow';
				arrow.textContent = '\u2192';
				item.appendChild(arrow);
				li.appendChild(item);
				grid.appendChild(li);
			}
			for (const act of cfg.altActivities) {
				const li = document.createElement('li');
				const item = document.createElement('div');
				item.className = 'alt-item';
				item.appendChild(document.createTextNode(act));
				li.appendChild(item);
				grid.appendChild(li);
			}
			promptPhase.appendChild(grid);
		}
		const continueBtn = document.createElement('button');
		continueBtn.className = 'continue-btn';
		continueBtn.textContent = 'Continue to ' + hostname;
		continueBtn.addEventListener('click', () => {
			sessionStorage.setItem(BYPASS_KEY, Date.now().toString());
			location.reload();
		});
		promptPhase.appendChild(continueBtn);
		body.appendChild(promptPhase);
		document.documentElement.style.display = '';
		let breathIndex = 1;
		const runBreathLabel = () => {
			if (breathIndex > cfg.nob) return;
			breathLabel.style.opacity = '0';
			setTimeout(() => {
				breathLabel.textContent = 'breathe in';
				breathLabel.style.opacity = '1';
			}, 500);
			setTimeout(() => { breathLabel.style.opacity = '0'; }, INHALE_MS);
			setTimeout(() => {
				breathLabel.textContent = 'breathe out';
				breathLabel.style.opacity = '1';
			}, INHALE_MS + HOLD_IN_MS + 400);
			setTimeout(() => {
				breathLabel.style.opacity = '0';
				breathIndex++;
				if (breathIndex <= cfg.nob) {
					breathCounter.textContent = breathIndex + ' / ' + cfg.nob;
				}
			}, INHALE_MS + HOLD_IN_MS + EXHALE_MS);
		};
		runBreathLabel();
		for (let i = 1; i < cfg.nob; i++) {
			setTimeout(runBreathLabel, i * BREATH_CYCLE_MS);
		}
		setTimeout(() => {
			const breath = document.getElementById('pp-breath');
			const prompt = document.getElementById('pp-prompt');
			if (!breath || !prompt) return;
			breath.style.opacity = '0';
			setTimeout(() => {
				breath.style.display = 'none';
				prompt.style.display = 'flex';
				requestAnimationFrame(() => {
					prompt.style.opacity = '1';
				});
			}, 800);
		}, totalBreathMs - 200);
	};
	requestAnimationFrame(takeover);
})();