Compare commits
6 Commits
a781deccdd
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 55c9be761c | |||
| d776870d35 | |||
| 3e1cc94258 | |||
| 7ed7d22b6d | |||
| 91be1b1751 | |||
| 8434a6888f |
+94
-49
@@ -237,6 +237,36 @@ body {
|
|||||||
box-shadow: 0 4px 12px rgba(0, 122, 255, 0.3);
|
box-shadow: 0 4px 12px rgba(0, 122, 255, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* On narrow screens, switch to horizontal scrolling so all 6 tabs stay
|
||||||
|
on a single row instead of breaking into a tall stack. */
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
.main-nav {
|
||||||
|
display: flex !important;
|
||||||
|
flex-wrap: nowrap !important;
|
||||||
|
overflow-x: auto !important;
|
||||||
|
overflow-y: hidden !important;
|
||||||
|
scroll-snap-type: x proximity;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
scrollbar-width: none;
|
||||||
|
grid-template-columns: none !important;
|
||||||
|
gap: 4px !important;
|
||||||
|
padding: 6px !important;
|
||||||
|
}
|
||||||
|
.main-nav::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.nav-btn {
|
||||||
|
flex: 0 0 auto !important;
|
||||||
|
min-width: auto !important;
|
||||||
|
max-width: none !important;
|
||||||
|
white-space: nowrap !important;
|
||||||
|
scroll-snap-align: start;
|
||||||
|
padding: 10px 14px !important;
|
||||||
|
font-size: 0.88rem !important;
|
||||||
|
grid-column: auto !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* ============ Panels ============ */
|
/* ============ Panels ============ */
|
||||||
.panel {
|
.panel {
|
||||||
background: var(--glass-bg);
|
background: var(--glass-bg);
|
||||||
@@ -305,6 +335,10 @@ body {
|
|||||||
input[type="text"],
|
input[type="text"],
|
||||||
input[type="password"],
|
input[type="password"],
|
||||||
input[type="email"],
|
input[type="email"],
|
||||||
|
input[type="number"],
|
||||||
|
input[type="search"],
|
||||||
|
input[type="tel"],
|
||||||
|
input[type="url"],
|
||||||
select,
|
select,
|
||||||
textarea {
|
textarea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -312,7 +346,9 @@ textarea {
|
|||||||
background: var(--bg-secondary);
|
background: var(--bg-secondary);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
font-size: 1rem;
|
/* 16px minimum prevents iOS Safari from zooming the page when an input
|
||||||
|
receives focus. */
|
||||||
|
font-size: 16px;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
transition: all var(--transition-fast);
|
transition: all var(--transition-fast);
|
||||||
@@ -332,6 +368,17 @@ textarea {
|
|||||||
font-family: 'SF Mono', 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
font-family: 'SF Mono', 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Hide the up/down spinner on number inputs so the field looks the same
|
||||||
|
as the text inputs (and the actual hit area isn't squeezed). */
|
||||||
|
input[type="number"]::-webkit-outer-spin-button,
|
||||||
|
input[type="number"]::-webkit-inner-spin-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
input[type="number"] {
|
||||||
|
-moz-appearance: textfield;
|
||||||
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
appearance: none;
|
appearance: none;
|
||||||
@@ -3228,34 +3275,13 @@ select {
|
|||||||
transform: translateX(16px);
|
transform: translateX(16px);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Navigation - grid layout for mobile */
|
/* Mobile navigation tweaks — horizontal-scroll layout is defined at the
|
||||||
|
top of this file (@media max-width: 720px). Here we only adjust the
|
||||||
|
outer margin for the smaller breakpoint. */
|
||||||
.main-nav {
|
.main-nav {
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, 1fr);
|
|
||||||
gap: 4px;
|
|
||||||
padding: 6px;
|
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-btn {
|
|
||||||
padding: 10px 6px;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
text-align: center;
|
|
||||||
min-width: 0;
|
|
||||||
flex: none;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Make settings button span 2 columns for 5 items grid */
|
|
||||||
.nav-btn:nth-child(4) {
|
|
||||||
grid-column: 1 / 2;
|
|
||||||
}
|
|
||||||
.nav-btn:nth-child(5) {
|
|
||||||
grid-column: 2 / 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel {
|
.panel {
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
@@ -3580,6 +3606,11 @@ select {
|
|||||||
|
|
||||||
input[type="text"],
|
input[type="text"],
|
||||||
input[type="password"],
|
input[type="password"],
|
||||||
|
input[type="email"],
|
||||||
|
input[type="number"],
|
||||||
|
input[type="search"],
|
||||||
|
input[type="tel"],
|
||||||
|
input[type="url"],
|
||||||
textarea,
|
textarea,
|
||||||
select {
|
select {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
@@ -3844,20 +3875,9 @@ select {
|
|||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-nav {
|
/* Mobile nav layout is defined in the top-level @media (max-width: 720px)
|
||||||
grid-template-columns: repeat(2, 1fr);
|
— horizontal scroll with nowrap. We don't need to redefine it for
|
||||||
gap: 4px;
|
480px; everything inherits. */
|
||||||
}
|
|
||||||
|
|
||||||
/* Adjust nav buttons for 2-column grid */
|
|
||||||
.nav-btn:nth-child(5) {
|
|
||||||
grid-column: 1 / 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-btn {
|
|
||||||
padding: 10px 4px;
|
|
||||||
font-size: 0.7rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel {
|
.panel {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
@@ -3918,9 +3938,11 @@ select {
|
|||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Keep nav button text readable even on tiny phones, but smaller than
|
||||||
|
720px breakpoint. */
|
||||||
.nav-btn {
|
.nav-btn {
|
||||||
font-size: 0.65rem;
|
font-size: 0.78rem !important;
|
||||||
padding: 8px 2px;
|
padding: 9px 11px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel {
|
.panel {
|
||||||
@@ -3976,14 +3998,7 @@ select {
|
|||||||
|
|
||||||
/* Landscape phones */
|
/* Landscape phones */
|
||||||
@media (max-width: 768px) and (orientation: landscape) {
|
@media (max-width: 768px) and (orientation: landscape) {
|
||||||
.main-nav {
|
/* Navigation uses the horizontal-scroll layout from the 720px block. */
|
||||||
grid-template-columns: repeat(5, 1fr);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-btn:nth-child(4),
|
|
||||||
.nav-btn:nth-child(5) {
|
|
||||||
grid-column: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.floating-action-bar {
|
.floating-action-bar {
|
||||||
bottom: 6px;
|
bottom: 6px;
|
||||||
@@ -4127,6 +4142,36 @@ select {
|
|||||||
@media (max-width: 800px) {
|
@media (max-width: 800px) {
|
||||||
.badge-editor-layout {
|
.badge-editor-layout {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
/* Keep the canvas visible while the user scrolls down through the
|
||||||
|
controls — sticky to the top of the viewport. The thumbnail shrinks
|
||||||
|
to ~62vw so the controls below have room to breathe. */
|
||||||
|
.badge-canvas-wrapper {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 20;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
padding: 8px 0 6px;
|
||||||
|
margin: 0 -8px;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
#badge-canvas {
|
||||||
|
max-width: min(62vw, 320px);
|
||||||
|
}
|
||||||
|
.badge-canvas-hint {
|
||||||
|
font-size: 0.78em;
|
||||||
|
}
|
||||||
|
.badge-controls {
|
||||||
|
padding-top: 4px;
|
||||||
|
}
|
||||||
|
/* Stack form rows tightly on phones. */
|
||||||
|
.badge-color-row {
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.badge-color-swatch {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -208,6 +208,10 @@ if (isset($_GET['logout'])) {
|
|||||||
<span class="btn-icon-text">📤</span> Загрузить
|
<span class="btn-icon-text">📤</span> Загрузить
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<label class="checkbox-label compact" style="margin-top:8px;">
|
||||||
|
<input type="checkbox" id="chk-unlock-photo-limit">
|
||||||
|
<span>Больше 9 фото (для Telegram). В VK уйдут только первые 9.</span>
|
||||||
|
</label>
|
||||||
<div id="post-photos-preview" class="photos-preview combined-preview">
|
<div id="post-photos-preview" class="photos-preview combined-preview">
|
||||||
<p class="placeholder">Нажмите кнопку выше чтобы добавить фото</p>
|
<p class="placeholder">Нажмите кнопку выше чтобы добавить фото</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -685,6 +689,29 @@ https://live.staticflickr.com/65535/12345678901_abcdef1234_b.jpg"></textarea>
|
|||||||
<input type="text" id="badge-nickname-value" maxlength="40" placeholder="Никнейм">
|
<input type="text" id="badge-nickname-value" maxlength="40" placeholder="Никнейм">
|
||||||
<span class="hint">По умолчанию — из настроек.</span>
|
<span class="hint">По умолчанию — из настроек.</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" id="badge-nick-as-price">
|
||||||
|
<span>Использовать как цену — добавить ₽</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Положение надписи:</label>
|
||||||
|
<div class="badge-radio-row">
|
||||||
|
<label class="radio-label">
|
||||||
|
<input type="radio" name="badge-nick-position" value="auto" checked>
|
||||||
|
<span>Авто</span>
|
||||||
|
</label>
|
||||||
|
<label class="radio-label">
|
||||||
|
<input type="radio" name="badge-nick-position" value="bottom">
|
||||||
|
<span>Снизу</span>
|
||||||
|
</label>
|
||||||
|
<label class="radio-label">
|
||||||
|
<input type="radio" name="badge-nick-position" value="top">
|
||||||
|
<span>Сверху</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Размер текста: <span id="badge-nick-size-value">100%</span></label>
|
<label>Размер текста: <span id="badge-nick-size-value">100%</span></label>
|
||||||
<input type="range" id="badge-nick-size" min="60" max="160" step="5" value="100">
|
<input type="range" id="badge-nick-size" min="60" max="160" step="5" value="100">
|
||||||
|
|||||||
@@ -210,15 +210,55 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
|
|
||||||
// ============ STATE ============
|
// ============ STATE ============
|
||||||
|
|
||||||
const MAX_PHOTOS = 9;
|
// Soft limit for safe cross-platform posting. Can be lifted with the
|
||||||
|
// "Больше 9 фото" toggle on the posting page; VK gets a confirm warning
|
||||||
|
// because its album endpoint only accepts up to 9 attachments.
|
||||||
|
const DEFAULT_MAX_PHOTOS = 9;
|
||||||
|
const HARD_MAX_PHOTOS = 99;
|
||||||
|
|
||||||
|
function isPhotoLimitUnlocked() {
|
||||||
|
return localStorage.getItem('vh_unlock_9') === '1';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMaxPhotos() {
|
||||||
|
return isPhotoLimitUnlocked() ? HARD_MAX_PHOTOS : DEFAULT_MAX_PHOTOS;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backward compat for code that still reads MAX_PHOTOS as a constant —
|
||||||
|
// we keep the name but evaluate dynamically through getter usage below.
|
||||||
|
const MAX_PHOTOS = DEFAULT_MAX_PHOTOS;
|
||||||
|
|
||||||
|
// Sync the "more than 9 photos" toggle with localStorage and refresh counters
|
||||||
|
// whenever it changes.
|
||||||
|
function bindPhotoLimitToggle() {
|
||||||
|
const chk = document.getElementById('chk-unlock-photo-limit');
|
||||||
|
if (!chk) return;
|
||||||
|
chk.checked = isPhotoLimitUnlocked();
|
||||||
|
chk.addEventListener('change', () => {
|
||||||
|
if (chk.checked) {
|
||||||
|
localStorage.setItem('vh_unlock_9', '1');
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem('vh_unlock_9');
|
||||||
|
// If currently over 9 photos, warn — we won't trim automatically.
|
||||||
|
if (getTotalPhotosCount() > DEFAULT_MAX_PHOTOS) {
|
||||||
|
showNotification(`Сейчас выбрано ${getTotalPhotosCount()} фото. Удалите лишние перед публикацией.`, 'info');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateSelectionUI();
|
||||||
|
updatePostingPreview();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Bind after DOM is ready — we're already inside DOMContentLoaded.
|
||||||
|
bindPhotoLimitToggle();
|
||||||
|
|
||||||
function getTotalPhotosCount() {
|
function getTotalPhotosCount() {
|
||||||
return state.selectedPhotos.length + state.uploadedFiles.length;
|
return state.selectedPhotos.length + state.uploadedFiles.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
function canAddPhotos(count = 1) {
|
function canAddPhotos(count = 1) {
|
||||||
if (getTotalPhotosCount() + count > MAX_PHOTOS) {
|
const max = getMaxPhotos();
|
||||||
showNotification(`Максимум ${MAX_PHOTOS} фото/видео`, 'error');
|
if (getTotalPhotosCount() + count > max) {
|
||||||
|
showNotification(`Максимум ${max} фото/видео`, 'error');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -327,8 +367,9 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
// Check photo limit
|
// Check photo limit
|
||||||
if (getTotalPhotosCount() >= MAX_PHOTOS) {
|
const maxNow = getMaxPhotos();
|
||||||
showNotification(`Максимум ${MAX_PHOTOS} фото/видео`, 'error');
|
if (getTotalPhotosCount() >= maxNow) {
|
||||||
|
showNotification(`Максимум ${maxNow} фото/видео`, 'error');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -441,7 +482,16 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
const ALBUM_PREFS_KEY = 'vh_album_prefs';
|
const ALBUM_PREFS_KEY = 'vh_album_prefs';
|
||||||
const CACHE_TTL = 60 * 60 * 1000; // 1 hour
|
const CACHE_TTL = 60 * 60 * 1000; // 1 hour
|
||||||
|
|
||||||
|
// Returns just the albums array (back-compat with callers that destructure
|
||||||
|
// an array). The full pagination object is available via getAlbumCacheFull.
|
||||||
function getAlbumCache() {
|
function getAlbumCache() {
|
||||||
|
const full = getAlbumCacheFull();
|
||||||
|
return full ? full.albums : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns { albums, page, pages, total } or null. Pagination info may be
|
||||||
|
// missing on legacy caches.
|
||||||
|
function getAlbumCacheFull() {
|
||||||
try {
|
try {
|
||||||
const cached = localStorage.getItem(ALBUM_CACHE_KEY);
|
const cached = localStorage.getItem(ALBUM_CACHE_KEY);
|
||||||
if (!cached) return null;
|
if (!cached) return null;
|
||||||
@@ -450,12 +500,26 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
localStorage.removeItem(ALBUM_CACHE_KEY);
|
localStorage.removeItem(ALBUM_CACHE_KEY);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
// Handle both old format (array) and new format (object with albums property)
|
|
||||||
const albums = data.albums;
|
const albums = data.albums;
|
||||||
if (Array.isArray(albums)) {
|
if (Array.isArray(albums)) {
|
||||||
return albums;
|
// Legacy flat-array format — pagination unknown.
|
||||||
|
return { albums, page: 1, pages: 1, total: albums.length };
|
||||||
} else if (albums && Array.isArray(albums.albums)) {
|
} else if (albums && Array.isArray(albums.albums)) {
|
||||||
return albums.albums;
|
// Older nested format.
|
||||||
|
return {
|
||||||
|
albums: albums.albums,
|
||||||
|
page: albums.page || 1,
|
||||||
|
pages: albums.pages || 1,
|
||||||
|
total: albums.total || albums.albums.length,
|
||||||
|
};
|
||||||
|
} else if (Array.isArray(data.list)) {
|
||||||
|
// Current format: stored as { list, page, pages, total }.
|
||||||
|
return {
|
||||||
|
albums: data.list,
|
||||||
|
page: data.page || 1,
|
||||||
|
pages: data.pages || 1,
|
||||||
|
total: data.total || data.list.length,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -465,12 +529,21 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
|
|
||||||
function setAlbumCache(albumsOrData) {
|
function setAlbumCache(albumsOrData) {
|
||||||
try {
|
try {
|
||||||
// Accept either array or object with albums property
|
let payload;
|
||||||
const albums = Array.isArray(albumsOrData) ? albumsOrData : (albumsOrData.albums || []);
|
if (Array.isArray(albumsOrData)) {
|
||||||
localStorage.setItem(ALBUM_CACHE_KEY, JSON.stringify({
|
payload = { list: albumsOrData, page: 1, pages: 1, total: albumsOrData.length };
|
||||||
albums: albums,
|
} else if (albumsOrData && Array.isArray(albumsOrData.albums)) {
|
||||||
timestamp: Date.now()
|
payload = {
|
||||||
}));
|
list: albumsOrData.albums,
|
||||||
|
page: albumsOrData.page || 1,
|
||||||
|
pages: albumsOrData.pages || 1,
|
||||||
|
total: albumsOrData.total || albumsOrData.albums.length,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
payload.timestamp = Date.now();
|
||||||
|
localStorage.setItem(ALBUM_CACHE_KEY, JSON.stringify(payload));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('Failed to cache albums:', e);
|
console.warn('Failed to cache albums:', e);
|
||||||
}
|
}
|
||||||
@@ -574,17 +647,18 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
function updateSelectionUI() {
|
function updateSelectionUI() {
|
||||||
const count = state.selectedPhotos.length;
|
const count = state.selectedPhotos.length;
|
||||||
const total = getTotalPhotosCount();
|
const total = getTotalPhotosCount();
|
||||||
|
const maxNow = getMaxPhotos();
|
||||||
|
|
||||||
// Update counter with limit info (gallery floating bar)
|
// Update counter with limit info (gallery floating bar)
|
||||||
if (selectedCountEl) {
|
if (selectedCountEl) {
|
||||||
selectedCountEl.textContent = `${total}/${MAX_PHOTOS}`;
|
selectedCountEl.textContent = `${total}/${maxNow}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update counter on posting page
|
// Update counter on posting page
|
||||||
const photoCounter = document.getElementById('photo-counter');
|
const photoCounter = document.getElementById('photo-counter');
|
||||||
if (photoCounter) {
|
if (photoCounter) {
|
||||||
photoCounter.textContent = `${total}/${MAX_PHOTOS}`;
|
photoCounter.textContent = `${total}/${maxNow}`;
|
||||||
photoCounter.classList.toggle('at-limit', total >= MAX_PHOTOS);
|
photoCounter.classList.toggle('at-limit', total >= maxNow);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show/hide floating action bar
|
// Show/hide floating action bar
|
||||||
@@ -600,10 +674,11 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
function updatePostingPreview() {
|
function updatePostingPreview() {
|
||||||
// Always update photo counter
|
// Always update photo counter
|
||||||
const total = getTotalPhotosCount();
|
const total = getTotalPhotosCount();
|
||||||
|
const maxNow = getMaxPhotos();
|
||||||
const photoCounter = document.getElementById('photo-counter');
|
const photoCounter = document.getElementById('photo-counter');
|
||||||
if (photoCounter) {
|
if (photoCounter) {
|
||||||
photoCounter.textContent = `${total}/${MAX_PHOTOS}`;
|
photoCounter.textContent = `${total}/${maxNow}`;
|
||||||
photoCounter.classList.toggle('at-limit', total >= MAX_PHOTOS);
|
photoCounter.classList.toggle('at-limit', total >= maxNow);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!postPhotosPreview) return;
|
if (!postPhotosPreview) return;
|
||||||
@@ -981,22 +1056,40 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
refreshAlbumsSilently();
|
refreshAlbumsSilently();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh albums in background without showing loading to user
|
// Refresh albums in background without showing loading to user.
|
||||||
|
// IMPORTANT: also saves pagination state and arms the infinite-scroll
|
||||||
|
// observer, since the cached-render path skips both.
|
||||||
async function refreshAlbumsSilently() {
|
async function refreshAlbumsSilently() {
|
||||||
if (state.isLoadingAlbums) return;
|
if (state.isLoadingAlbums) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('api.php?action=flickr_albums');
|
const response = await fetch('api.php?action=flickr_albums&page=1&per_page=50');
|
||||||
if (!response.ok) return;
|
if (!response.ok) return;
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (data.albums && data.albums.length > 0) {
|
if (data.albums && data.albums.length > 0) {
|
||||||
setAlbumCache(data.albums);
|
state.allAlbums = data.albums;
|
||||||
|
state.albumsPage = data.page || 1;
|
||||||
|
state.albumsTotalPages = data.pages || 1;
|
||||||
|
state.albumsTotal = data.total || data.albums.length;
|
||||||
|
|
||||||
|
setAlbumCache({
|
||||||
|
albums: data.albums,
|
||||||
|
page: state.albumsPage,
|
||||||
|
pages: state.albumsTotalPages,
|
||||||
|
total: state.albumsTotal,
|
||||||
|
});
|
||||||
window._cachedAlbums = data.albums;
|
window._cachedAlbums = data.albums;
|
||||||
|
|
||||||
// Only re-render if on albums view (photos view is hidden)
|
// Only re-render if on albums view (photos view is hidden)
|
||||||
if (photosView?.classList.contains('hidden')) {
|
if (photosView?.classList.contains('hidden')) {
|
||||||
renderAlbumsGrid(data.albums);
|
renderAlbumsGrid(data.albums);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Arm infinite scroll if there are more pages available.
|
||||||
|
if (state.albumsPage < state.albumsTotalPages) {
|
||||||
|
setupAlbumsInfiniteScroll();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('Silent refresh failed:', error.message);
|
console.log('Silent refresh failed:', error.message);
|
||||||
@@ -1028,25 +1121,23 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
|
|
||||||
// Check cache first
|
// Check cache first
|
||||||
if (!forceRefresh) {
|
if (!forceRefresh) {
|
||||||
const cached = getAlbumCache();
|
const full = getAlbumCacheFull();
|
||||||
if (cached && cached.albums) {
|
if (full && full.albums && full.albums.length > 0) {
|
||||||
console.log('Using cached albums:', cached.albums.length);
|
console.log('Using cached albums:', full.albums.length);
|
||||||
state.allAlbums = cached.albums;
|
state.allAlbums = full.albums;
|
||||||
state.albumsPage = cached.page || 1;
|
state.albumsPage = full.page;
|
||||||
state.albumsTotalPages = cached.pages || 1;
|
state.albumsTotalPages = full.pages;
|
||||||
state.albumsTotal = cached.total || cached.albums.length;
|
state.albumsTotal = full.total;
|
||||||
window._cachedAlbums = cached.albums;
|
window._cachedAlbums = full.albums;
|
||||||
renderAlbumsGrid(cached.albums);
|
renderAlbumsGrid(full.albums);
|
||||||
showNotification(`Загружено ${cached.albums.length} альбомов (из кеша)`, 'success');
|
showNotification(`Загружено ${full.albums.length} альбомов (из кеша)`, 'success');
|
||||||
|
// Arm scroll observer if there might be more pages, even when
|
||||||
|
// pagination info is missing from a legacy cache.
|
||||||
setupAlbumsInfiniteScroll();
|
setupAlbumsInfiniteScroll();
|
||||||
return;
|
// Refresh in background to get accurate page count.
|
||||||
} else if (cached && Array.isArray(cached)) {
|
if (full.pages <= 1) {
|
||||||
// Old cache format compatibility
|
setTimeout(() => refreshAlbumsSilently(), 500);
|
||||||
console.log('Using old cached albums:', cached.length);
|
}
|
||||||
state.allAlbums = cached;
|
|
||||||
window._cachedAlbums = cached;
|
|
||||||
renderAlbumsGrid(cached);
|
|
||||||
showNotification(`Загружено ${cached.length} альбомов (из кеша)`, 'success');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1223,6 +1314,19 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
showNotification('Ошибка загрузки альбомов: ' + error.message, 'error');
|
showNotification('Ошибка загрузки альбомов: ' + error.message, 'error');
|
||||||
} finally {
|
} finally {
|
||||||
state.isLoadingMoreAlbums = false;
|
state.isLoadingMoreAlbums = false;
|
||||||
|
// IntersectionObserver fires on STATE CHANGE — if the sentinel never left the
|
||||||
|
// viewport (e.g. small thumbnails) it won't auto-trigger again. Re-check here.
|
||||||
|
if (state.albumsPage < state.albumsTotalPages) {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
const sentinel = document.getElementById('albums-scroll-sentinel');
|
||||||
|
if (!sentinel || state.isLoadingMoreAlbums) return;
|
||||||
|
const rect = sentinel.getBoundingClientRect();
|
||||||
|
const viewportH = window.innerHeight || document.documentElement.clientHeight;
|
||||||
|
if (rect.top < viewportH + 200) {
|
||||||
|
loadMoreAlbums();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1276,6 +1380,25 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
albumsScrollObserver.observe(sentinel);
|
albumsScrollObserver.observe(sentinel);
|
||||||
|
|
||||||
|
// Fallback scroll listener — covers cases where IntersectionObserver
|
||||||
|
// doesn't refire after a load (e.g. sentinel stays in viewport).
|
||||||
|
if (!window._albumsScrollFallbackBound) {
|
||||||
|
window._albumsScrollFallbackBound = true;
|
||||||
|
const checkSentinel = () => {
|
||||||
|
if (state.isLoadingMoreAlbums) return;
|
||||||
|
if (state.albumsPage >= state.albumsTotalPages) return;
|
||||||
|
const s = document.getElementById('albums-scroll-sentinel');
|
||||||
|
if (!s) return;
|
||||||
|
const rect = s.getBoundingClientRect();
|
||||||
|
const viewportH = window.innerHeight || document.documentElement.clientHeight;
|
||||||
|
if (rect.top < viewportH + 200) {
|
||||||
|
loadMoreAlbums();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('scroll', checkSentinel, { passive: true });
|
||||||
|
window.addEventListener('resize', checkSentinel, { passive: true });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============ LOAD PHOTOS (with infinite scroll) ============
|
// ============ LOAD PHOTOS (with infinite scroll) ============
|
||||||
@@ -1598,16 +1721,17 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
// ============ FLOATING ACTION BAR ============
|
// ============ FLOATING ACTION BAR ============
|
||||||
|
|
||||||
document.getElementById('btn-select-all')?.addEventListener('click', () => {
|
document.getElementById('btn-select-all')?.addEventListener('click', () => {
|
||||||
|
const maxNow = getMaxPhotos();
|
||||||
document.querySelectorAll('.photo-item').forEach(item => {
|
document.querySelectorAll('.photo-item').forEach(item => {
|
||||||
if (getTotalPhotosCount() >= MAX_PHOTOS) return;
|
if (getTotalPhotosCount() >= maxNow) return;
|
||||||
const photo = JSON.parse(item.dataset.photoData);
|
const photo = JSON.parse(item.dataset.photoData);
|
||||||
if (!state.selectedPhotos.find(p => p.id === photo.id)) {
|
if (!state.selectedPhotos.find(p => p.id === photo.id)) {
|
||||||
state.selectedPhotos.push(photo);
|
state.selectedPhotos.push(photo);
|
||||||
item.classList.add('selected');
|
item.classList.add('selected');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (getTotalPhotosCount() >= MAX_PHOTOS) {
|
if (getTotalPhotosCount() >= maxNow) {
|
||||||
showNotification(`Выбрано максимум ${MAX_PHOTOS} фото`, 'info');
|
showNotification(`Выбрано максимум ${maxNow} фото`, 'info');
|
||||||
}
|
}
|
||||||
updateSelectionUI();
|
updateSelectionUI();
|
||||||
saveSelectedPhotos();
|
saveSelectedPhotos();
|
||||||
@@ -2357,6 +2481,23 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
.filter(f => f.url && !f.uploading)
|
.filter(f => f.url && !f.uploading)
|
||||||
.map(f => ({ url: f.url, type: f.type }));
|
.map(f => ({ url: f.url, type: f.type }));
|
||||||
|
|
||||||
|
// VK has a max of 9 attachments per post. If we're posting to VK and
|
||||||
|
// the total media exceeds that, ask the user whether to truncate.
|
||||||
|
const totalMedia = photoUrls.length + uploadedFileUrls.length;
|
||||||
|
if (postToVk && totalMedia > DEFAULT_MAX_PHOTOS) {
|
||||||
|
const ok = confirm(
|
||||||
|
`Выбрано ${totalMedia} фото/видео, а VK принимает максимум ${DEFAULT_MAX_PHOTOS} в одной публикации.\n\n` +
|
||||||
|
`В VK уйдут только первые ${DEFAULT_MAX_PHOTOS}. В Telegram отправятся все.\n\nПродолжить?`
|
||||||
|
);
|
||||||
|
if (!ok) {
|
||||||
|
if (btnSendPost) {
|
||||||
|
btnSendPost.disabled = false;
|
||||||
|
btnSendPost.textContent = 'Опубликовать';
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check cross-promo settings
|
// Check cross-promo settings
|
||||||
const crossPromoEnabled = document.getElementById('chk-cross-promo')?.checked;
|
const crossPromoEnabled = document.getElementById('chk-cross-promo')?.checked;
|
||||||
// Get settings from localStorage, but also check current input values as fallback
|
// Get settings from localStorage, but also check current input values as fallback
|
||||||
@@ -2396,12 +2537,26 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
const results = {};
|
const results = {};
|
||||||
|
|
||||||
for (const platform of platforms) {
|
for (const platform of platforms) {
|
||||||
|
// VK: hard-trim combined media to the platform limit.
|
||||||
|
let photosForPlatform = photoUrls;
|
||||||
|
let uploadedForPlatform = uploadedFileUrls;
|
||||||
|
if (platform.type === 'vk') {
|
||||||
|
const cap = DEFAULT_MAX_PHOTOS;
|
||||||
|
if (photoUrls.length >= cap) {
|
||||||
|
photosForPlatform = photoUrls.slice(0, cap);
|
||||||
|
uploadedForPlatform = [];
|
||||||
|
} else {
|
||||||
|
photosForPlatform = photoUrls;
|
||||||
|
uploadedForPlatform = uploadedFileUrls.slice(0, cap - photoUrls.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('action', 'multi_post');
|
formData.append('action', 'multi_post');
|
||||||
formData.append('platforms', JSON.stringify([platform]));
|
formData.append('platforms', JSON.stringify([platform]));
|
||||||
formData.append('text', platform.type === 'telegram' ? textForTelegram : textForVk);
|
formData.append('text', platform.type === 'telegram' ? textForTelegram : textForVk);
|
||||||
formData.append('photos', JSON.stringify(photoUrls));
|
formData.append('photos', JSON.stringify(photosForPlatform));
|
||||||
formData.append('uploaded_files', JSON.stringify(uploadedFileUrls));
|
formData.append('uploaded_files', JSON.stringify(uploadedForPlatform));
|
||||||
formData.append('parse_mode', parseMode);
|
formData.append('parse_mode', parseMode);
|
||||||
|
|
||||||
const response = await fetch('api.php', { method: 'POST', body: formData });
|
const response = await fetch('api.php', { method: 'POST', body: formData });
|
||||||
@@ -4210,13 +4365,20 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
|
|
||||||
// Auto-load albums on page init
|
// Auto-load albums on page init
|
||||||
if (albumsGrid) {
|
if (albumsGrid) {
|
||||||
const cachedAlbums = getAlbumCache();
|
const fullCache = getAlbumCacheFull();
|
||||||
if (cachedAlbums && cachedAlbums.length > 0) {
|
if (fullCache && fullCache.albums.length > 0) {
|
||||||
// Use cache first for instant display
|
// Use cache for instant display, and prime pagination state so
|
||||||
console.log('Loading cached albums:', cachedAlbums.length);
|
// the infinite-scroll observer can engage immediately.
|
||||||
window._cachedAlbums = cachedAlbums;
|
console.log('Loading cached albums:', fullCache.albums.length);
|
||||||
renderAlbumsGrid(cachedAlbums);
|
state.allAlbums = fullCache.albums;
|
||||||
// Silently refresh in background
|
state.albumsPage = fullCache.page;
|
||||||
|
state.albumsTotalPages = fullCache.pages;
|
||||||
|
state.albumsTotal = fullCache.total;
|
||||||
|
window._cachedAlbums = fullCache.albums;
|
||||||
|
renderAlbumsGrid(fullCache.albums);
|
||||||
|
setupAlbumsInfiniteScroll();
|
||||||
|
// Silently refresh in background — this also corrects pagination
|
||||||
|
// info if the cache is from a legacy build that lost it.
|
||||||
setTimeout(() => refreshAlbumsSilently(), 2000);
|
setTimeout(() => refreshAlbumsSilently(), 2000);
|
||||||
} else {
|
} else {
|
||||||
// No cache or empty - load from API
|
// No cache or empty - load from API
|
||||||
@@ -4265,6 +4427,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
nickSizeValue: document.getElementById('badge-nick-size-value'),
|
nickSizeValue: document.getElementById('badge-nick-size-value'),
|
||||||
nickEdge: document.getElementById('badge-nick-edge'),
|
nickEdge: document.getElementById('badge-nick-edge'),
|
||||||
nickEdgeValue: document.getElementById('badge-nick-edge-value'),
|
nickEdgeValue: document.getElementById('badge-nick-edge-value'),
|
||||||
|
nickAsPrice: document.getElementById('badge-nick-as-price'),
|
||||||
btnDownload: document.getElementById('btn-badge-download'),
|
btnDownload: document.getElementById('btn-badge-download'),
|
||||||
btnRemove: document.getElementById('btn-badge-remove'),
|
btnRemove: document.getElementById('btn-badge-remove'),
|
||||||
batchActions: document.getElementById('badge-batch-actions'),
|
batchActions: document.getElementById('badge-batch-actions'),
|
||||||
@@ -4494,10 +4657,24 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (item.showNickname) {
|
if (item.showNickname) {
|
||||||
const nick = (item.nickname || '').trim();
|
let nick = (item.nickname || '').trim();
|
||||||
|
// "Use as price" formatting: append ₽; if the text is purely digits,
|
||||||
|
// also insert thousands separators so it reads as a proper price.
|
||||||
|
if (item.nickAsPrice && nick) {
|
||||||
|
const digits = nick.replace(/[^\d]/g, '');
|
||||||
|
if (digits && /^\d+$/.test(nick)) {
|
||||||
|
nick = parseInt(digits, 10).toLocaleString('ru-RU') + ' ₽';
|
||||||
|
} else if (!nick.includes('₽')) {
|
||||||
|
nick = nick + ' ₽';
|
||||||
|
}
|
||||||
|
}
|
||||||
if (nick) {
|
if (nick) {
|
||||||
// Auto-position: if price occupies the bottom, draw nickname as a top arc.
|
// Position: manual override (top/bottom) or auto (top when price occupies the bottom).
|
||||||
drawNicknameArc(c, size, nick, item, hasPrice ? 'top' : 'bottom');
|
let pos;
|
||||||
|
if (item.nickPosition === 'top') pos = 'top';
|
||||||
|
else if (item.nickPosition === 'bottom') pos = 'bottom';
|
||||||
|
else pos = hasPrice ? 'top' : 'bottom';
|
||||||
|
drawNicknameArc(c, size, nick, item, pos);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4563,6 +4740,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
nickColor: '#FFFFFF', // text color (hex)
|
nickColor: '#FFFFFF', // text color (hex)
|
||||||
nickSize: 100, // 60..160 (% scale of font)
|
nickSize: 100, // 60..160 (% scale of font)
|
||||||
nickEdge: 14, // 0..30: percent inset from circle edge (0 = at edge)
|
nickEdge: 14, // 0..30: percent inset from circle edge (0 = at edge)
|
||||||
|
nickPosition: 'auto', // 'auto' | 'top' | 'bottom'
|
||||||
|
nickAsPrice: false, // append ₽ + format digits as price
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4627,6 +4806,10 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
el.nickSizeValue.textContent = item.nickSize + '%';
|
el.nickSizeValue.textContent = item.nickSize + '%';
|
||||||
el.nickEdge.value = item.nickEdge;
|
el.nickEdge.value = item.nickEdge;
|
||||||
el.nickEdgeValue.textContent = item.nickEdge;
|
el.nickEdgeValue.textContent = item.nickEdge;
|
||||||
|
el.nickAsPrice.checked = !!item.nickAsPrice;
|
||||||
|
document.querySelectorAll('input[name="badge-nick-position"]').forEach(r => {
|
||||||
|
r.checked = r.value === (item.nickPosition || 'auto');
|
||||||
|
});
|
||||||
renderItemsGrid();
|
renderItemsGrid();
|
||||||
renderPreview();
|
renderPreview();
|
||||||
}
|
}
|
||||||
@@ -4962,6 +5145,26 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
renderPreview();
|
renderPreview();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Nickname "use as price" — append ₽ to the rendered text.
|
||||||
|
el.nickAsPrice.addEventListener('change', () => {
|
||||||
|
const item = currentItem();
|
||||||
|
if (!item) return;
|
||||||
|
item.nickAsPrice = el.nickAsPrice.checked;
|
||||||
|
renderPreview();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Nickname position (auto / top / bottom).
|
||||||
|
document.querySelectorAll('input[name="badge-nick-position"]').forEach(r => {
|
||||||
|
r.addEventListener('change', () => {
|
||||||
|
const item = currentItem();
|
||||||
|
if (!item) return;
|
||||||
|
if (r.checked) {
|
||||||
|
item.nickPosition = r.value;
|
||||||
|
renderPreview();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
el.btnRemove.addEventListener('click', () => {
|
el.btnRemove.addEventListener('click', () => {
|
||||||
const item = currentItem();
|
const item = currentItem();
|
||||||
if (item) removeItem(item.id);
|
if (item) removeItem(item.id);
|
||||||
@@ -4998,26 +5201,13 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
el.btnFromFlickr.addEventListener('click', async () => {
|
el.btnFromFlickr.addEventListener('click', async () => {
|
||||||
const photos = state.selectedPhotos || [];
|
const photos = state.selectedPhotos || [];
|
||||||
if (photos.length === 0) {
|
if (photos.length === 0) {
|
||||||
showNotification('Сначала выберите фото в Галерее', 'info');
|
showNotification('Сначала выберите фото в Галерее (галочками)', 'info');
|
||||||
document.querySelector('.nav-btn[data-tab="gallery"]')?.click();
|
document.querySelector('.nav-btn[data-tab="gallery"]')?.click();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const added = await autoImportGallerySelection(true);
|
||||||
// Skip duplicates already in badge list
|
if (added === 0) {
|
||||||
const existingSrcs = new Set(badgeState.items.map(it => it.src));
|
showNotification('Все выбранные фото уже в списке', 'info');
|
||||||
let added = 0;
|
|
||||||
for (const photo of photos) {
|
|
||||||
const url = (photo.urls && (photo.urls.large || photo.urls.medium640 || photo.urls.medium)) || null;
|
|
||||||
if (!url) continue;
|
|
||||||
if (existingSrcs.has(url)) continue;
|
|
||||||
const item = await addItemFromUrl(url, photo.title || ('photo_' + photo.id), true);
|
|
||||||
if (!badgeState.activeId) selectItem(item.id);
|
|
||||||
added++;
|
|
||||||
}
|
|
||||||
if (added > 0) {
|
|
||||||
showNotification(`Добавлено ${added} фото из Flickr`, 'success');
|
|
||||||
} else {
|
|
||||||
showNotification('Нечего добавлять (уже в списке)', 'info');
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -5249,10 +5439,37 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
|
|
||||||
// ---------- Tab activation ----------
|
// ---------- Tab activation ----------
|
||||||
|
|
||||||
|
// Pull in currently-selected gallery photos that aren't already in the
|
||||||
|
// badge list. Called when opening the badge tab and when clicking the
|
||||||
|
// "from Flickr" button. Returns the number of items added.
|
||||||
|
async function autoImportGallerySelection(showFeedback) {
|
||||||
|
const photos = state.selectedPhotos || [];
|
||||||
|
if (photos.length === 0) return 0;
|
||||||
|
const existingSrcs = new Set(badgeState.items.map(it => it.src));
|
||||||
|
const toAdd = [];
|
||||||
|
for (const photo of photos) {
|
||||||
|
const url = (photo.urls && (photo.urls.large || photo.urls.medium640 || photo.urls.medium)) || null;
|
||||||
|
if (!url || existingSrcs.has(url)) continue;
|
||||||
|
toAdd.push({ url, name: photo.title || ('photo_' + photo.id) });
|
||||||
|
}
|
||||||
|
if (toAdd.length === 0) return 0;
|
||||||
|
for (const t of toAdd) {
|
||||||
|
const item = await addItemFromUrl(t.url, t.name, true);
|
||||||
|
if (!badgeState.activeId) selectItem(item.id);
|
||||||
|
}
|
||||||
|
if (showFeedback) {
|
||||||
|
showNotification(`Добавлено ${toAdd.length} фото из Галереи`, 'success');
|
||||||
|
}
|
||||||
|
return toAdd.length;
|
||||||
|
}
|
||||||
|
|
||||||
document.querySelectorAll('.nav-btn[data-tab="badge"]').forEach(btn => {
|
document.querySelectorAll('.nav-btn[data-tab="badge"]').forEach(btn => {
|
||||||
btn.addEventListener('click', () => {
|
btn.addEventListener('click', () => {
|
||||||
loadBadgeSettings();
|
loadBadgeSettings();
|
||||||
loadHistory();
|
loadHistory();
|
||||||
|
// Auto-import gallery selections so users see them right away
|
||||||
|
// without having to click "Выбранные в галерее Flickr".
|
||||||
|
autoImportGallerySelection(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user