Javi

Tool: background gallery

Dec 25th, 2025
122
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 10.60 KB | None | 0 0
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>Art Gallery</title>
  7. <style>
  8. * {
  9. margin: 0;
  10. padding: 0;
  11. box-sizing: border-box;
  12. }
  13.  
  14. body {
  15. font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
  16. overflow: hidden;
  17. background: #000;
  18. }
  19.  
  20. #background {
  21. position: fixed;
  22. top: 0;
  23. left: 0;
  24. width: 100%;
  25. height: 100%;
  26. background-size: contain;
  27. background-position: center;
  28. background-repeat: no-repeat;
  29. transition: opacity 1s ease-in-out;
  30. }
  31.  
  32. #overlay {
  33. position: fixed;
  34. top: 0;
  35. left: 0;
  36. width: 100%;
  37. height: 100%;
  38. background: linear-gradient(to bottom, rgba(0,0,0,0.3), rgba(0,0,0,0.5));
  39. display: flex;
  40. flex-direction: column;
  41. justify-content: space-between;
  42. padding: 40px;
  43. color: white;
  44. text-shadow: 2px 2px 8px rgba(0,0,0,0.8);
  45. opacity: 0;
  46. transition: opacity 0.5s ease;
  47. pointer-events: none;
  48. }
  49.  
  50. #overlay.visible {
  51. opacity: 1;
  52. pointer-events: auto;
  53. }
  54.  
  55. #info {
  56. max-width: 600px;
  57. }
  58.  
  59. h1 {
  60. font-size: 2.5rem;
  61. margin-bottom: 10px;
  62. font-weight: 600;
  63. }
  64.  
  65. .source {
  66. font-size: 1rem;
  67. opacity: 0.9;
  68. margin-bottom: 20px;
  69. }
  70.  
  71. .description {
  72. font-size: 1.1rem;
  73. line-height: 1.6;
  74. opacity: 0.95;
  75. }
  76.  
  77. #controls {
  78. display: flex;
  79. gap: 15px;
  80. align-items: center;
  81. }
  82.  
  83. button {
  84. background: rgba(255, 255, 255, 0.2);
  85. backdrop-filter: blur(10px);
  86. border: 1px solid rgba(255, 255, 255, 0.3);
  87. color: white;
  88. padding: 12px 24px;
  89. border-radius: 8px;
  90. cursor: pointer;
  91. font-size: 1rem;
  92. transition: all 0.3s ease;
  93. }
  94.  
  95. button:hover {
  96. background: rgba(255, 255, 255, 0.3);
  97. transform: translateY(-2px);
  98. }
  99.  
  100. #loading {
  101. position: fixed;
  102. top: 50%;
  103. left: 50%;
  104. transform: translate(-50%, -50%);
  105. color: white;
  106. font-size: 1.2rem;
  107. }
  108.  
  109. #error {
  110. position: fixed;
  111. top: 50%;
  112. left: 50%;
  113. transform: translate(-50%, -50%);
  114. color: white;
  115. text-align: center;
  116. background: rgba(0, 0, 0, 0.8);
  117. padding: 30px;
  118. border-radius: 12px;
  119. display: none;
  120. }
  121. </style>
  122. </head>
  123. <body>
  124. <div id="background"></div>
  125. <div id="overlay">
  126. <div id="info">
  127. <h1 id="title">Loading...</h1>
  128. <div class="source" id="source"></div>
  129. <div class="description" id="description"></div>
  130. </div>
  131. <div id="controls">
  132. <button id="nextBtn">Next Image</button>
  133. <button id="linkBtn">View Original</button>
  134. </div>
  135. </div>
  136. <div id="loading">Loading art...</div>
  137. <div id="error">
  138. <h2>Unable to load art</h2>
  139. <p>Please check your Miniflux API settings</p>
  140. </div>
  141.  
  142. <script>
  143. const MINIFLUX_URL = 'https://miniflux.mininube.com';
  144. const CATEGORY_ID = 3; // Art category
  145.  
  146. // Get API key from query param or use default
  147. const urlParams = new URLSearchParams(window.location.search);
  148. const API_KEY = urlParams.get('key') || 'YOUR_API_KEY_HERE';
  149.  
  150. let entries = [];
  151. let currentIndex = 0;
  152.  
  153. const overlay = document.getElementById('overlay');
  154.  
  155. // Toggle overlay on click anywhere
  156. document.addEventListener('click', (e) => {
  157. // Don't toggle if clicking on buttons
  158. if (e.target.tagName === 'BUTTON') return;
  159. overlay.classList.toggle('visible');
  160. });
  161.  
  162. // Track image appearances
  163. const getAppearances = () => {
  164. const stored = localStorage.getItem('artAppearances');
  165. return stored ? JSON.parse(stored) : {};
  166. };
  167.  
  168. const incrementAppearance = (entryId) => {
  169. const appearances = getAppearances();
  170. appearances[entryId] = (appearances[entryId] || 0) + 1;
  171. localStorage.setItem('artAppearances', JSON.stringify(appearances));
  172. };
  173.  
  174. const selectNextEntry = () => {
  175. if (entries.length === 0) return null;
  176.  
  177. const appearances = getAppearances();
  178.  
  179. // Find the minimum appearance count
  180. let minCount = Infinity;
  181. entries.forEach(entry => {
  182. const count = appearances[entry.id] || 0;
  183. if (count < minCount) minCount = count;
  184. });
  185.  
  186. // Filter entries with minimum appearance count
  187. const leastShownEntries = entries.filter(entry => {
  188. const count = appearances[entry.id] || 0;
  189. return count === minCount;
  190. });
  191.  
  192. // Pick a random entry from the least shown ones
  193. const randomIndex = Math.floor(Math.random() * leastShownEntries.length);
  194. return leastShownEntries[randomIndex];
  195. };
  196.  
  197. const fetchEntries = async () => {
  198. try {
  199. const params = new URLSearchParams({
  200. limit: 50,
  201. order: 'published_at',
  202. direction: 'desc'
  203. });
  204.  
  205. const response = await fetch(
  206. `${MINIFLUX_URL}/v1/categories/${CATEGORY_ID}/entries?${params.toString()}`,
  207. {
  208. headers: {
  209. 'X-Auth-Token': API_KEY
  210. }
  211. }
  212. );
  213.  
  214. if (!response.ok) {
  215. throw new Error(`HTTP error! status: ${response.status}`);
  216. }
  217.  
  218. const data = await response.json();
  219.  
  220. // Filter entries that have images
  221. entries = data.entries.filter(entry => {
  222. const hasImageEnclosure = entry.enclosures.some(enc =>
  223. enc.mime_type.startsWith('image/')
  224. );
  225. if (hasImageEnclosure) return true;
  226.  
  227. // Check for images in content
  228. const tempDiv = document.createElement('div');
  229. tempDiv.innerHTML = entry.content;
  230. return tempDiv.querySelector('img') !== null;
  231. });
  232.  
  233. if (entries.length === 0) {
  234. throw new Error('No entries with images found');
  235. }
  236.  
  237. document.getElementById('loading').style.display = 'none';
  238. const firstEntry = selectNextEntry();
  239. showEntry(firstEntry);
  240. } catch (error) {
  241. console.error('Failed to fetch entries:', error);
  242. document.getElementById('loading').style.display = 'none';
  243. document.getElementById('error').style.display = 'block';
  244. }
  245. };
  246.  
  247. const getEntryImage = (entry) => {
  248. // First try enclosures
  249. const imageEnclosure = entry.enclosures.find(enc =>
  250. enc.mime_type.startsWith('image/')
  251. );
  252. if (imageEnclosure) return imageEnclosure.url;
  253.  
  254. // Fallback to first image in content
  255. const tempDiv = document.createElement('div');
  256. tempDiv.innerHTML = entry.content;
  257. const firstImg = tempDiv.querySelector('img');
  258. return firstImg?.src;
  259. };
  260.  
  261. const showEntry = (entry) => {
  262. if (!entry) return;
  263.  
  264. const imageSrc = getEntryImage(entry);
  265.  
  266. const bg = document.getElementById('background');
  267. bg.style.opacity = '0';
  268.  
  269. // Load image to check dimensions
  270. const img = new Image();
  271. img.onload = () => {
  272. const viewportWidth = window.innerWidth;
  273. const viewportHeight = window.innerHeight;
  274.  
  275. // Check if image is at least 80% of viewport size
  276. const isLargeEnough = img.width >= viewportWidth * 0.8 &&
  277. img.height >= viewportHeight * 0.8;
  278.  
  279. bg.style.backgroundSize = isLargeEnough ? 'cover' : 'contain';
  280. bg.style.backgroundImage = `url(${imageSrc})`;
  281. bg.style.opacity = '1';
  282. };
  283. img.src = imageSrc;
  284.  
  285. setTimeout(() => {
  286. // Fallback in case image doesn't load
  287. if (bg.style.opacity === '0') {
  288. bg.style.backgroundImage = `url(${imageSrc})`;
  289. bg.style.opacity = '1';
  290. }
  291. }, 300);
  292.  
  293. document.getElementById('title').textContent = entry.title;
  294. document.getElementById('source').textContent = entry.feed.title;
  295.  
  296. // Extract text from content (remove HTML tags)
  297. const tempDiv = document.createElement('div');
  298. tempDiv.innerHTML = entry.content;
  299. const text = tempDiv.textContent.trim().substring(0, 200);
  300. document.getElementById('description').textContent = text ? text + '...' : '';
  301.  
  302. // Update link button
  303. document.getElementById('linkBtn').onclick = () => {
  304. window.open(entry.url, '_blank');
  305. };
  306.  
  307. // Track this appearance
  308. incrementAppearance(entry.id);
  309. };
  310.  
  311. document.getElementById('nextBtn').addEventListener('click', () => {
  312. const nextEntry = selectNextEntry();
  313. showEntry(nextEntry);
  314. });
  315.  
  316. // Auto-advance every 30 seconds (optional)
  317. // setInterval(() => {
  318. // const nextIndex = (currentIndex + 1) % entries.length;
  319. // showEntry(nextIndex);
  320. // }, 30000);
  321.  
  322. fetchEntries();
  323. </script>
  324. </body>
  325. </html>
Advertisement
Add Comment
Please, Sign In to add comment