diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py index 4951d15..29e56b5 100644 --- a/basicswap/basicswap.py +++ b/basicswap/basicswap.py @@ -11349,6 +11349,11 @@ class BasicSwap(BaseApp, UIApp): if filter_include_sent is not None and filter_include_sent is not True: query_suffix += " AND was_sent = 0" + filter_auto_accept_type = filters.get("auto_accept_type", None) + if filter_auto_accept_type and filter_auto_accept_type != "any": + query_suffix += " AND auto_accept_type = :filter_auto_accept_type" + query_data["filter_auto_accept_type"] = int(filter_auto_accept_type) + query_suffix += getOrderByStr(filters) limit = filters.get("limit", None) diff --git a/basicswap/js_server.py b/basicswap/js_server.py index 6761c3a..61f6fec 100644 --- a/basicswap/js_server.py +++ b/basicswap/js_server.py @@ -278,6 +278,7 @@ def js_offers(self, url_split, post_string, is_json, sent=False) -> bytes: "is_public": o.addr_to == swap_client.network_addr or o.addr_to.strip() == "", } + offer_data["auto_accept_type"] = getattr(o, "auto_accept_type", 0) if with_extra_info: offer_data["amount_negotiable"] = o.amount_negotiable offer_data["rate_negotiable"] = o.rate_negotiable @@ -293,6 +294,7 @@ def js_offers(self, url_split, post_string, is_json, sent=False) -> bytes: offer_data["feerate_to"] = o.to_feerate offer_data["automation_strat_id"] = getattr(o, "auto_accept_type", 0) + offer_data["auto_accept_type"] = getattr(o, "auto_accept_type", 0) if o.was_sent: try: diff --git a/basicswap/static/css/style.css b/basicswap/static/css/style.css index 8897efb..7c098c0 100644 --- a/basicswap/static/css/style.css +++ b/basicswap/static/css/style.css @@ -365,3 +365,147 @@ select.disabled-select-enabled { #toggle-auto-refresh[data-enabled="true"] { @apply bg-green-500 hover:bg-green-600 focus:ring-green-300; } + +/* Multi-select dropdown styles */ +.multi-select-dropdown::-webkit-scrollbar { + width: 12px; +} + +.multi-select-dropdown::-webkit-scrollbar-track { + background: #f1f1f1; + border-radius: 6px; +} + +.multi-select-dropdown::-webkit-scrollbar-thumb { + background: #888; + border-radius: 6px; +} + +.multi-select-dropdown::-webkit-scrollbar-thumb:hover { + background: #555; +} + +.dark .multi-select-dropdown::-webkit-scrollbar-track { + background: #374151; +} + +.dark .multi-select-dropdown::-webkit-scrollbar-thumb { + background: #6b7280; +} + +.dark .multi-select-dropdown::-webkit-scrollbar-thumb:hover { + background: #9ca3af; +} + +.multi-select-dropdown input[type="checkbox"]:focus { + outline: none !important; + box-shadow: none !important; + border-color: inherit !important; +} + +.multi-select-dropdown label:focus-within { + outline: none !important; + box-shadow: none !important; +} + +#coin_to_button:focus, +#coin_from_button:focus { + outline: none !important; + box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.3) !important; + border-color: #3b82f6 !important; +} + + .coin-badge { + background: #3b82f6; + color: white; + padding: 2px 8px; + border-radius: 12px; + font-size: 12px; + display: inline-flex; + align-items: center; + gap: 4px; + margin: 2px; + } + .coin-badge .remove { + cursor: pointer; + font-weight: bold; + opacity: 0.7; + margin-left: 4px; + } + .coin-badge .remove:hover { + opacity: 1; + } + .multi-select-dropdown { + max-height: 300px; + overflow-y: auto; + z-index: 9999 !important; + position: fixed !important; + min-width: 200px; + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3); + } + .multi-select-dropdown::-webkit-scrollbar { + width: 6px; + } + .multi-select-dropdown::-webkit-scrollbar-track { + background: #f1f1f1; + } + .multi-select-dropdown::-webkit-scrollbar-thumb { + background: #888; + border-radius: 3px; + } + .multi-select-dropdown::-webkit-scrollbar-thumb:hover { + background: #555; + } + .dark .multi-select-dropdown::-webkit-scrollbar-track { + background: #374151; + } + .dark .multi-select-dropdown::-webkit-scrollbar-thumb { + background: #6b7280; + } + .dropdown-container { + position: relative; + z-index: 1; + } + .dropdown-container.open { + z-index: 9999; + } + .filter-button-text { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .multi-select-dropdown input[type="checkbox"] { + outline: none !important; + box-shadow: none !important; + border: 1px solid #d1d5db; + border-radius: 3px; + } + .multi-select-dropdown input[type="checkbox"]:focus { + outline: none !important; + box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.3) !important; + border-color: #3b82f6 !important; + } + .multi-select-dropdown input[type="checkbox"]:checked { + background-color: #3b82f6 !important; + border-color: #3b82f6 !important; + } + .dark .multi-select-dropdown input[type="checkbox"] { + border-color: #6b7280; + background-color: #374151; + } + .dark .multi-select-dropdown input[type="checkbox"]:focus { + border-color: #3b82f6 !important; + box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.3) !important; + } + .dark .multi-select-dropdown input[type="checkbox"]:checked { + background-color: #3b82f6 !important; + border-color: #3b82f6 !important; + } + + .multi-select-dropdown label { + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + } diff --git a/basicswap/static/js/amm_tables.js b/basicswap/static/js/amm_tables.js index f6545f9..f22802f 100644 --- a/basicswap/static/js/amm_tables.js +++ b/basicswap/static/js/amm_tables.js @@ -61,64 +61,9 @@ const AmmTablesManager = (function() { function getImageFilename(coinSymbol) { if (!coinSymbol) return 'Unknown.png'; - const coinNameToSymbol = { - 'bitcoin': 'BTC', - 'monero': 'XMR', - 'particl': 'PART', - 'particl anon': 'PART_ANON', - 'particl blind': 'PART_BLIND', - 'litecoin': 'LTC', - 'bitcoincash': 'BCH', - 'bitcoin cash': 'BCH', - 'firo': 'FIRO', - 'zcoin': 'FIRO', - 'pivx': 'PIVX', - 'dash': 'DASH', - 'ethereum': 'ETH', - 'dogecoin': 'DOGE', - 'decred': 'DCR', - 'namecoin': 'NMC', - 'zano': 'ZANO', - 'wownero': 'WOW' - }; - - let normalizedInput = coinSymbol.toLowerCase(); - - if (coinNameToSymbol[normalizedInput]) { - normalizedInput = coinNameToSymbol[normalizedInput]; - } - - const normalizedSymbol = normalizedInput.toUpperCase(); - - if (normalizedSymbol === 'FIRO' || normalizedSymbol === 'ZCOIN') return 'Firo.png'; - if (normalizedSymbol === 'BCH' || normalizedSymbol === 'BITCOINCASH') return 'Bitcoin-Cash.png'; - if (normalizedSymbol === 'PART_ANON' || normalizedSymbol === 'PARTICL_ANON') return 'Particl.png'; - if (normalizedSymbol === 'PART_BLIND' || normalizedSymbol === 'PARTICL_BLIND') return 'Particl.png'; - - if (window.CoinManager && window.CoinManager.getCoinBySymbol) { - const coin = window.CoinManager.getCoinBySymbol(normalizedSymbol); - if (coin && coin.image) return coin.image; - } - - const coinImages = { - 'BTC': 'Bitcoin.png', - 'XMR': 'Monero.png', - 'PART': 'Particl.png', - 'LTC': 'Litecoin.png', - 'FIRO': 'Firo.png', - 'PIVX': 'PIVX.png', - 'DASH': 'Dash.png', - 'ETH': 'Ethereum.png', - 'DOGE': 'Dogecoin.png', - 'DCR': 'Decred.png', - 'NMC': 'Namecoin.png', - 'ZANO': 'Zano.png', - 'WOW': 'Wownero.png' - }; - - const result = coinImages[normalizedSymbol] || 'Unknown.png'; - debugLog(`Coin symbol: ${coinSymbol}, normalized: ${normalizedSymbol}, image: ${result}`); - return result; + const icon = window.CoinManager.getCoinIcon(coinSymbol); + debugLog(`CoinManager returned icon: ${icon} for ${coinSymbol}`); + return icon || 'Unknown.png'; } function getCoinDisplayName(coinId) { diff --git a/basicswap/static/js/bids_available.js b/basicswap/static/js/bids_available.js index 4cd1e03..1120189 100644 --- a/basicswap/static/js/bids_available.js +++ b/basicswap/static/js/bids_available.js @@ -352,7 +352,7 @@ const createBidTableRow = async (bid) => {
${bid.coin_from} @@ -361,7 +361,7 @@ const createBidTableRow = async (bid) => { ${bid.coin_to} diff --git a/basicswap/static/js/bids_sentreceived.js b/basicswap/static/js/bids_sentreceived.js index b15e2a4..c6f2110 100644 --- a/basicswap/static/js/bids_sentreceived.js +++ b/basicswap/static/js/bids_sentreceived.js @@ -509,6 +509,21 @@ function coinMatches(offerCoin, filterCoin) { return offerCoin === filterCoin; } + if (filterCoin.includes(' ') || offerCoin.includes(' ')) { + const filterFirstWord = filterCoin.split(' ')[0]; + const offerFirstWord = offerCoin.split(' ')[0]; + + if (filterFirstWord === 'bitcoin' && offerFirstWord === 'bitcoin') { + const filterHasCash = filterCoin.includes('cash'); + const offerHasCash = offerCoin.includes('cash'); + return filterHasCash === offerHasCash; + } + + if (filterFirstWord === offerFirstWord && filterFirstWord.length > 4) { + return true; + } + } + return false; } diff --git a/basicswap/static/js/modules/coin-manager.js b/basicswap/static/js/modules/coin-manager.js index 773d5bf..a84db04 100644 --- a/basicswap/static/js/modules/coin-manager.js +++ b/basicswap/static/js/modules/coin-manager.js @@ -47,7 +47,7 @@ const CoinManager = (function() { usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30, - icon: 'Bitcoin-Cash.png' + icon: 'Bitcoin%20Cash.png' }, { symbol: 'PIVX', @@ -235,6 +235,31 @@ const CoinManager = (function() { const coin = getCoinByAnyIdentifier(coinIdentifier); if (!coin) return coinIdentifier.toLowerCase(); return coin.coingeckoId; + }, + getCoinIcon: function(identifier) { + if (!identifier) return null; + + const normalizedId = identifier.toString().toLowerCase().trim(); + if (normalizedId === 'particl anon' || normalizedId === 'part_anon' || normalizedId === 'particl_anon') { + return 'Particl.png'; + } + if (normalizedId === 'particl blind' || normalizedId === 'part_blind' || normalizedId === 'particl_blind') { + return 'Particl.png'; + } + if (normalizedId === 'litecoin mweb' || normalizedId === 'ltc_mweb' || normalizedId === 'litecoin_mweb') { + return 'Litecoin.png'; + } + + const coin = getCoinByAnyIdentifier(identifier); + if (coin && coin.icon) { + return coin.icon; + } + + const capitalizedName = identifier.toString().split(' ') + .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) + .join('%20'); + + return `${capitalizedName}.png`; } }; })(); diff --git a/basicswap/static/js/modules/config-manager.js b/basicswap/static/js/modules/config-manager.js index d4df53e..7cd1bd3 100644 --- a/basicswap/static/js/modules/config-manager.js +++ b/basicswap/static/js/modules/config-manager.js @@ -152,6 +152,21 @@ const ConfigManager = (function() { if (filterCoin === 'particl' && particlVariants.includes(offerCoin)) { return true; } + + if (filterCoin.includes(' ') || offerCoin.includes(' ')) { + const filterFirstWord = filterCoin.split(' ')[0]; + const offerFirstWord = offerCoin.split(' ')[0]; + + if (filterFirstWord === 'bitcoin' && offerFirstWord === 'bitcoin') { + const filterHasCash = filterCoin.includes('cash'); + const offerHasCash = offerCoin.includes('cash'); + return filterHasCash === offerHasCash; + } + + if (filterFirstWord === offerFirstWord && filterFirstWord.length > 4) { + return true; + } + } if (particlVariants.includes(filterCoin)) { return offerCoin === filterCoin; } diff --git a/basicswap/static/js/offers.js b/basicswap/static/js/offers.js index 37bc082..0873d5d 100644 --- a/basicswap/static/js/offers.js +++ b/basicswap/static/js/offers.js @@ -132,7 +132,6 @@ function initializeTableRateModule() { } function continueInitialization() { - updateCoinFilterImages(); fetchOffers().then(() => { applyFilters(); if (!isSentOffers) { @@ -157,65 +156,340 @@ function getValidOffers() { return []; } - const filteredData = filterAndSortData(); - return filteredData; + return jsonData; } function saveFilterSettings() { - const formData = new FormData(filterForm); - const filters = Object.fromEntries(formData); - const storageKey = isSentOffers ? 'sentOffersTableSettings' : 'networkOffersTableSettings'; + const selectedCoinTo = getSelectedCoins('coin_to'); + const selectedCoinFrom = getSelectedCoins('coin_from'); + + const statusSelect = document.getElementById('status'); + const sentFromSelect = document.getElementById('sent_from'); + const autoAcceptTypeSelect = document.getElementById('auto_accept_type'); + localStorage.setItem(storageKey, JSON.stringify({ - coin_to: filters.coin_to, - coin_from: filters.coin_from, - status: filters.status, - sent_from: filters.sent_from, + coin_to: selectedCoinTo, + coin_from: selectedCoinFrom, + status: statusSelect ? statusSelect.value : 'any', + sent_from: sentFromSelect ? sentFromSelect.value : 'any', + auto_accept_type: autoAcceptTypeSelect ? autoAcceptTypeSelect.value : 'any', sortColumn: currentSortColumn, sortDirection: currentSortDirection })); } -function coinMatches(offerCoin, filterCoin) { - if (!offerCoin || !filterCoin || filterCoin === 'any') return true; - offerCoin = offerCoin.toLowerCase(); - filterCoin = filterCoin.toLowerCase(); +function getSelectedCoins(filterType) { - if (offerCoin === filterCoin) return true; - - if ((offerCoin === 'firo' || offerCoin === 'zcoin') && - (filterCoin === 'firo' || filterCoin === 'zcoin')) { - return true; + const dropdown = document.getElementById(`${filterType}_dropdown`); + if (!dropdown) { + return ['any']; } - if ((offerCoin === 'bitcoincash' && filterCoin === 'bitcoin cash') || - (offerCoin === 'bitcoin cash' && filterCoin === 'bitcoincash')) { - return true; - } - const particlVariants = ['particl', 'particl anon', 'particl blind']; - if (filterCoin === 'particl' && particlVariants.includes(offerCoin)) { - return true; - } + const allCheckboxes = dropdown.querySelectorAll('input[type="checkbox"]'); - if (particlVariants.includes(filterCoin)) { - return offerCoin === filterCoin; - } + const selected = []; + allCheckboxes.forEach((cb) => { + if (cb.checked && cb.value !== 'any') { + selected.push(cb.value); + } + }); - return false; + return selected.length > 0 ? selected : ['any']; } -function filterAndSortData() { - const formData = new FormData(filterForm); - const filters = Object.fromEntries(formData); +function getCoinNameFromValue(value, filterType) { + if (value === 'any') { + return 'any'; + } + const dropdown = document.getElementById(`${filterType}_dropdown`); + if (!dropdown) { + return value; + } + + const checkbox = dropdown.querySelector(`input[value="${value}"]`); + if (checkbox) { + const label = checkbox.closest('label'); + const spans = label.querySelectorAll('span'); + + const coinSpan = spans[spans.length - 1]; + if (coinSpan) { + const text = coinSpan.textContent.trim(); + + const cleanText = text.replace(/\s*\(clear all\)\s*/, ''); + return cleanText; + } + } + + return value; +} + +function getCoinImage(coinName) { + return window.CoinManager.getCoinIcon(coinName); +} + +function updateFilterButtonText(filterType) { + const selected = getSelectedCoins(filterType); + const button = document.getElementById(`${filterType}_button`); + const textSpan = document.getElementById(`${filterType}_text`); + + if (!button || !textSpan) return; + + if (selected.length === 0 || (selected.length === 1 && selected[0] === 'any')) { + + const defaultText = filterType === 'coin_to' ? + (isSentOffers ? 'Filter Receiving' : 'Filter Bids') : + (isSentOffers ? 'Filter Sending' : 'Filter Offers'); + textSpan.textContent = defaultText; + } else { + const filterLabel = filterType === 'coin_to' ? + (isSentOffers ? 'Receiving' : 'Bids') : + (isSentOffers ? 'Sending' : 'Offers'); + textSpan.textContent = `Filter ${filterLabel} (${selected.length} selected)`; + } + + + button.style.width = '210px'; +} + +function updateCoinBadges(filterType) { + const selected = getSelectedCoins(filterType); + const badgesContainer = document.getElementById(`${filterType}_badges`); + const mainContainer = document.getElementById('selected_coins_container'); + + if (!badgesContainer) return; + + badgesContainer.innerHTML = ''; + + if (selected.length > 0 && !(selected.length === 1 && selected[0] === 'any')) { + selected.forEach(coinValue => { + const coinName = getCoinNameFromValue(coinValue, filterType); + const badge = document.createElement('span'); + + + const isBidsFilter = filterType === 'coin_to' && !isSentOffers; + const isOffersFilter = filterType === 'coin_from' && !isSentOffers; + const isReceivingFilter = filterType === 'coin_to' && isSentOffers; + const isSendingFilter = filterType === 'coin_from' && isSentOffers; + + let badgeClass = 'inline-flex items-center px-3 py-2 rounded-full text-sm font-medium mr-2 mb-1'; + if (isBidsFilter || isReceivingFilter) { + badgeClass += ' bg-blue-500 text-white dark:bg-blue-600 dark:text-white'; + } else { + badgeClass += ' bg-green-400 text-white dark:bg-green-500 dark:text-white'; + } + + badge.className = badgeClass + ' cursor-pointer hover:opacity-80'; + + + const coinImage = getCoinImage(coinName); + + badge.innerHTML = ` + ${coinName} + ${coinName} + + `; + + CleanupManager.addListener(badge, 'click', (e) => { + e.preventDefault(); + e.stopPropagation(); + removeCoinFilter(filterType, coinValue); + }); + + const closeBtn = badge.querySelector('.remove-coin-btn'); + if (closeBtn) { + CleanupManager.addListener(closeBtn, 'click', (e) => { + e.preventDefault(); + e.stopPropagation(); + removeCoinFilter(filterType, coinValue); + }); + } + + badgesContainer.appendChild(badge); + }); + + if (mainContainer) { + mainContainer.style.display = 'block'; + } + } else { + + const otherType = filterType === 'coin_to' ? 'coin_from' : 'coin_to'; + const otherSelected = getSelectedCoins(otherType); + if (otherSelected.length === 0 || (otherSelected.length === 1 && otherSelected[0] === 'any')) { + if (mainContainer) { + mainContainer.style.display = 'none'; + } + } + } +} + +function coinMatches(offerCoin, filterCoins) { + if (!offerCoin || !filterCoins) return true; + + if (typeof filterCoins === 'string') { + filterCoins = [filterCoins]; + } + + if (filterCoins.includes('any') || filterCoins.length === 0) return true; + + const normalizedOfferCoin = offerCoin.toLowerCase().trim(); + + return filterCoins.some(filterCoin => { + if (!filterCoin) return false; + + const normalizedFilterCoin = filterCoin.toLowerCase().trim(); + + if (normalizedOfferCoin === normalizedFilterCoin) { + return true; + } + + if ((normalizedOfferCoin === 'firo' || normalizedOfferCoin === 'zcoin') && + (normalizedFilterCoin === 'firo' || normalizedFilterCoin === 'zcoin')) { + return true; + } + + if ((normalizedOfferCoin === 'bitcoincash' && normalizedFilterCoin === 'bitcoin cash') || + (normalizedOfferCoin === 'bitcoin cash' && normalizedFilterCoin === 'bitcoincash')) { + return true; + } + + const particlVariants = ['particl', 'particl anon', 'particl blind']; + if (normalizedFilterCoin === 'particl' && particlVariants.includes(normalizedOfferCoin)) { + return true; + } + + if (particlVariants.includes(normalizedFilterCoin)) { + return normalizedOfferCoin === normalizedFilterCoin; + } + + if (normalizedFilterCoin.includes(' ') || normalizedOfferCoin.includes(' ')) { + const filterFirstWord = normalizedFilterCoin.split(' ')[0]; + const offerFirstWord = normalizedOfferCoin.split(' ')[0]; + + if (filterFirstWord === 'bitcoin' && offerFirstWord === 'bitcoin') { + const filterHasCash = normalizedFilterCoin.includes('cash'); + const offerHasCash = normalizedOfferCoin.includes('cash'); + return filterHasCash === offerHasCash; + } + + if (filterFirstWord === offerFirstWord && filterFirstWord.length > 4) { + return true; + } + } + + return false; + }); +} + +function toggleDropdown(filterType) { + const dropdown = document.getElementById(`${filterType}_dropdown`); + const button = document.getElementById(`${filterType}_button`); + const container = dropdown?.closest('.dropdown-container'); + + if (dropdown && button) { + const isVisible = dropdown.style.display !== 'none'; + hideAllDropdowns(); + + if (!isVisible) { + const buttonRect = button.getBoundingClientRect(); + dropdown.style.position = 'fixed'; + dropdown.style.top = (buttonRect.bottom + 4) + 'px'; + dropdown.style.left = buttonRect.left + 'px'; + dropdown.style.width = buttonRect.width + 'px'; + dropdown.style.display = 'block'; + + if (container) { + container.classList.add('open'); + } + } + } +} + +function hideDropdown(filterType) { + const dropdown = document.getElementById(`${filterType}_dropdown`); + const container = dropdown?.closest('.dropdown-container'); + + if (dropdown) { + dropdown.style.display = 'none'; + if (container) { + container.classList.remove('open'); + } + } +} + +function hideAllDropdowns() { + hideDropdown('coin_to'); + hideDropdown('coin_from'); +} + +function handleCoinCheckboxChange(filterType, checkbox) { + if (checkbox.value === 'any') { + + if (checkbox.checked) { + const dropdown = document.getElementById(`${filterType}_dropdown`); + if (dropdown) { + const otherCheckboxes = dropdown.querySelectorAll('input[type="checkbox"]:not([value="any"])'); + otherCheckboxes.forEach(cb => cb.checked = false); + } + } + } else { + + if (checkbox.checked) { + const dropdown = document.getElementById(`${filterType}_dropdown`); + if (dropdown) { + const anyCheckbox = dropdown.querySelector('input[value="any"]'); + if (anyCheckbox) anyCheckbox.checked = false; + } + } + } + + updateFilterButtonText(filterType); + updateCoinBadges(filterType); + applyFilters(); + updateClearFiltersButton(); +} + +function removeCoinFilter(filterType, coinValue) { + + const dropdown = document.getElementById(`${filterType}_dropdown`); + if (!dropdown) { + return; + } + + const checkbox = dropdown.querySelector(`input[value="${coinValue}"]`); + if (checkbox) { + checkbox.checked = false; + + + updateFilterButtonText(filterType); + updateCoinBadges(filterType); + applyFilters(); + updateClearFiltersButton(); + } +} + + +window.removeCoinFilter = removeCoinFilter; + +function filterAndSortData() { saveFilterSettings(); let filteredData = [...originalJsonData]; - const sentFromFilter = filters.sent_from || 'any'; + const statusSelect = document.getElementById('status'); + const sentFromSelect = document.getElementById('sent_from'); + const autoAcceptTypeSelect = document.getElementById('auto_accept_type'); + const selectedCoinTo = getSelectedCoins('coin_to'); + const selectedCoinFrom = getSelectedCoins('coin_from'); + + const sentFromFilter = sentFromSelect ? sentFromSelect.value : 'any'; filteredData = filteredData.filter(offer => { const isMatch = sentFromFilter === 'public' ? offer.is_public : sentFromFilter === 'private' ? !offer.is_public : @@ -223,37 +497,43 @@ function filterAndSortData() { return isMatch; }); + const autoAcceptTypeFilter = autoAcceptTypeSelect ? autoAcceptTypeSelect.value : 'any'; + if (autoAcceptTypeFilter !== 'any') { + filteredData = filteredData.filter(offer => { + const offerAutoAcceptType = offer.auto_accept_type !== undefined ? offer.auto_accept_type : 0; + return offerAutoAcceptType === parseInt(autoAcceptTypeFilter); + }); + } + filteredData = filteredData.filter(offer => { if (!isSentOffers && isOfferExpired(offer)) { return false; } - if (filters.coin_to && filters.coin_to !== 'any') { - const coinToSelect = document.getElementById('coin_to'); - const selectedOption = coinToSelect?.querySelector(`option[value="${filters.coin_to}"]`); - const coinName = selectedOption?.textContent.trim(); - if (coinName && !coinMatches(offer.coin_to, coinName)) { + if (selectedCoinTo.length > 0 && !(selectedCoinTo.length === 1 && selectedCoinTo[0] === 'any')) { + const coinNames = selectedCoinTo.map(value => getCoinNameFromValue(value, 'coin_to')); + const matches = coinMatches(offer.coin_to, coinNames); + if (!matches) { return false; } } - if (filters.coin_from && filters.coin_from !== 'any') { - const coinFromSelect = document.getElementById('coin_from'); - const selectedOption = coinFromSelect?.querySelector(`option[value="${filters.coin_from}"]`); - const coinName = selectedOption?.textContent.trim(); - if (coinName && !coinMatches(offer.coin_from, coinName)) { + if (selectedCoinFrom.length > 0 && !(selectedCoinFrom.length === 1 && selectedCoinFrom[0] === 'any')) { + const coinNames = selectedCoinFrom.map(value => getCoinNameFromValue(value, 'coin_from')); + const matches = coinMatches(offer.coin_from, coinNames); + if (!matches) { return false; } } - if (isSentOffers && filters.status && filters.status !== 'any') { + if (isSentOffers && statusSelect && statusSelect.value !== 'any') { const isExpired = offer.expire_at <= Math.floor(Date.now() / 1000); const isRevoked = Boolean(offer.is_revoked); let statusMatch = false; - switch (filters.status) { + switch (statusSelect.value) { case 'active': statusMatch = !isExpired && !isRevoked; break; @@ -554,6 +834,7 @@ function formatInitialData(data) { amount_negotiable: Boolean(offer.amount_negotiable), is_revoked: Boolean(offer.is_revoked), is_public: offer.is_public !== undefined ? Boolean(offer.is_public) : false, + auto_accept_type: offer.auto_accept_type !== undefined ? Number(offer.auto_accept_type) : 0, unique_id: `${offer.offer_id}_${offer.created_at}_${offer.coin_from}_${offer.coin_to}` })); } @@ -721,28 +1002,7 @@ function updateProfitLoss(row, fromCoin, toCoin, fromAmount, toAmount, isOwnOffe } function updateCoinFilterImages() { - const coinToSelect = document.getElementById('coin_to'); - const coinFromSelect = document.getElementById('coin_from'); - const coinToButton = document.getElementById('coin_to_button'); - const coinFromButton = document.getElementById('coin_from_button'); - - function updateButtonImage(select, button) { - const selectedOption = select.options[select.selectedIndex]; - const imagePath = selectedOption.getAttribute('data-image'); - if (imagePath && select.value !== 'any') { - button.style.backgroundImage = `url(${imagePath})`; - button.style.backgroundSize = '25px 25px'; - button.style.backgroundPosition = 'center'; - button.style.backgroundRepeat = 'no-repeat'; - } else { - button.style.backgroundImage = 'none'; - } - button.style.minWidth = '25px'; - button.style.minHeight = '25px'; - } - - updateButtonImage(coinToSelect, coinToButton); - updateButtonImage(coinFromSelect, coinFromButton); + return; } function updateClearFiltersButton() { @@ -799,11 +1059,7 @@ function cleanupTable() { } function handleNoOffersScenario() { - const formData = new FormData(filterForm); - const filters = Object.fromEntries(formData); - const hasActiveFilters = filters.coin_to !== 'any' || - filters.coin_from !== 'any' || - (filters.status && filters.status !== 'any'); + const hasFilters = hasActiveFilters(); stopRefreshAnimation(); @@ -812,7 +1068,7 @@ function handleNoOffersScenario() { cleanupRow(row); }); - if (hasActiveFilters) { + if (hasFilters) { offersBody.innerHTML = ` @@ -1726,13 +1982,8 @@ function applyFilters() { } function clearFilters() { - filterForm.reset(); - - const selectElements = filterForm.querySelectorAll('select'); - selectElements.forEach(select => { - select.value = 'any'; - const event = new Event('change', { bubbles: true }); - select.dispatchEvent(event); + document.querySelectorAll('.coin-to-checkbox, .coin-from-checkbox').forEach(checkbox => { + checkbox.checked = checkbox.value === 'any'; }); const statusSelect = document.getElementById('status'); @@ -1740,6 +1991,22 @@ function clearFilters() { statusSelect.value = 'any'; } + const sentFromSelect = document.getElementById('sent_from'); + if (sentFromSelect) { + sentFromSelect.value = 'any'; + } + + const autoAcceptTypeSelect = document.getElementById('auto_accept_type'); + if (autoAcceptTypeSelect) { + autoAcceptTypeSelect.value = 'any'; + } + + updateFilterButtonText('coin_to'); + updateFilterButtonText('coin_from'); + updateCoinBadges('coin_to'); + updateCoinBadges('coin_from'); + hideAllDropdowns(); + jsonData = [...originalJsonData]; currentPage = 1; @@ -1747,21 +2014,25 @@ function clearFilters() { localStorage.removeItem(storageKey); updateOffersTable(); - updateCoinFilterImages(); updateClearFiltersButton(); } function hasActiveFilters() { - const selectElements = filterForm.querySelectorAll('select'); - let hasChangedFilters = false; + const selectedCoinTo = getSelectedCoins('coin_to'); + const selectedCoinFrom = getSelectedCoins('coin_from'); - selectElements.forEach(select => { - if (select.value !== 'any') { - hasChangedFilters = true; - } - }); + const hasCoinToFilter = selectedCoinTo.length > 0 && !(selectedCoinTo.length === 1 && selectedCoinTo[0] === 'any'); + const hasCoinFromFilter = selectedCoinFrom.length > 0 && !(selectedCoinFrom.length === 1 && selectedCoinFrom[0] === 'any'); - return hasChangedFilters; + const statusSelect = document.getElementById('status'); + const sentFromSelect = document.getElementById('sent_from'); + const autoAcceptTypeSelect = document.getElementById('auto_accept_type'); + + const hasStatusFilter = statusSelect && statusSelect.value !== 'any'; + const hasSentFromFilter = sentFromSelect && sentFromSelect.value !== 'any'; + const hasAutoAcceptTypeFilter = autoAcceptTypeSelect && autoAcceptTypeSelect.value !== 'any'; + + return hasCoinToFilter || hasCoinFromFilter || hasStatusFilter || hasSentFromFilter || hasAutoAcceptTypeFilter; } function formatTimeLeft(timestamp) { @@ -1791,13 +2062,6 @@ function getCoinSymbolLowercase(coin) { } } -function coinMatches(offerCoin, filterCoin) { - if (window.CoinManager) { - return window.CoinManager.coinMatches(offerCoin, filterCoin); - } - return window.config.coinMatches(offerCoin, filterCoin); -} - function getProfitColorClass(percentage) { const numericPercentage = parseFloat(percentage); if (numericPercentage > 0) return 'text-green-500'; @@ -1872,25 +2136,51 @@ function initializeTableEvents() { }); } - const coinToSelect = document.getElementById('coin_to'); - const coinFromSelect = document.getElementById('coin_from'); + const coinToButton = document.getElementById('coin_to_button'); + const coinFromButton = document.getElementById('coin_from_button'); + const coinToDropdown = document.getElementById('coin_to_dropdown'); + const coinFromDropdown = document.getElementById('coin_from_dropdown'); const statusSelect = document.getElementById('status'); const sentFromSelect = document.getElementById('sent_from'); - if (coinToSelect) { - CleanupManager.addListener(coinToSelect, 'change', () => { - applyFilters(); - updateCoinFilterImages(); + + if (coinToButton && coinToDropdown) { + CleanupManager.addListener(coinToButton, 'click', (e) => { + e.stopPropagation(); + toggleDropdown('coin_to'); + }); + + const coinToCheckboxes = coinToDropdown.querySelectorAll('.coin-to-checkbox'); + coinToCheckboxes.forEach(checkbox => { + CleanupManager.addListener(checkbox, 'change', (e) => { + handleCoinCheckboxChange('coin_to', e.target); + }); }); } - if (coinFromSelect) { - CleanupManager.addListener(coinFromSelect, 'change', () => { - applyFilters(); - updateCoinFilterImages(); + + if (coinFromButton && coinFromDropdown) { + CleanupManager.addListener(coinFromButton, 'click', (e) => { + e.stopPropagation(); + toggleDropdown('coin_from'); + }); + + const coinFromCheckboxes = coinFromDropdown.querySelectorAll('.coin-from-checkbox'); + coinFromCheckboxes.forEach(checkbox => { + CleanupManager.addListener(checkbox, 'change', (e) => { + handleCoinCheckboxChange('coin_from', e.target); + }); }); } + CleanupManager.addListener(window, 'resize', () => { + hideAllDropdowns(); + }); + + CleanupManager.addListener(window, 'scroll', () => { + hideAllDropdowns(); + }); + if (statusSelect) { CleanupManager.addListener(statusSelect, 'change', () => { applyFilters(); @@ -1903,11 +2193,17 @@ function initializeTableEvents() { }); } + const autoAcceptTypeSelect = document.getElementById('auto_accept_type'); + if (autoAcceptTypeSelect) { + CleanupManager.addListener(autoAcceptTypeSelect, 'change', () => { + applyFilters(); + }); + } + const clearFiltersBtn = document.getElementById('clearFilters'); if (clearFiltersBtn) { CleanupManager.addListener(clearFiltersBtn, 'click', () => { clearFilters(); - updateCoinFilterImages(); }); } @@ -2133,7 +2429,12 @@ async function initializeTableAndData() { updateClearFiltersButton(); initializeTableEvents(); initializeTooltips(); - updateCoinFilterImages(); + + + updateFilterButtonText('coin_to'); + updateFilterButtonText('coin_from'); + updateCoinBadges('coin_to'); + updateCoinBadges('coin_from'); try { await fetchOffers(); @@ -2152,7 +2453,23 @@ function loadSavedSettings() { if (saved) { const settings = JSON.parse(saved); - ['coin_to', 'coin_from', 'status', 'sent_from'].forEach(id => { + ['coin_to', 'coin_from'].forEach(filterType => { + const savedCoins = settings[filterType]; + if (savedCoins && Array.isArray(savedCoins)) { + + document.querySelectorAll(`.${filterType}-checkbox`).forEach(cb => cb.checked = false); + + savedCoins.forEach(coinValue => { + const checkbox = document.querySelector(`.${filterType}-checkbox[value="${coinValue}"]`); + if (checkbox) checkbox.checked = true; + }); + + updateFilterButtonText(filterType); + updateCoinBadges(filterType); + } + }); + + ['status', 'sent_from', 'auto_accept_type'].forEach(id => { const element = document.getElementById(id); if (element && settings[id]) element.value = settings[id]; }); @@ -2308,7 +2625,6 @@ document.addEventListener('DOMContentLoaded', async function() { filterForm.querySelectorAll('select').forEach(select => { CleanupManager.addListener(select, 'change', () => { applyFilters(); - updateCoinFilterImages(); updateClearFiltersButton(); }); }); @@ -2319,6 +2635,23 @@ document.addEventListener('DOMContentLoaded', async function() { CleanupManager.addListener(clearFiltersBtn, 'click', clearFilters); } + CleanupManager.addListener(document, 'click', (e) => { + const coinToDropdown = document.getElementById('coin_to_dropdown'); + const coinFromDropdown = document.getElementById('coin_from_dropdown'); + + if (coinToDropdown && coinToDropdown.style.display !== 'none') { + if (!e.target.closest('#coin_to_button') && !e.target.closest('#coin_to_dropdown')) { + hideDropdown('coin_to'); + } + } + + if (coinFromDropdown && coinFromDropdown.style.display !== 'none') { + if (!e.target.closest('#coin_from_button') && !e.target.closest('#coin_from_dropdown')) { + hideDropdown('coin_from'); + } + } + }); + const rowTimeInterval = setInterval(updateRowTimes, 30000); if (CleanupManager.registerResource) { CleanupManager.registerResource('rowTimeInterval', rowTimeInterval, () => { diff --git a/basicswap/static/js/swaps_in_progress.js b/basicswap/static/js/swaps_in_progress.js index 5e8dc5d..3637dad 100644 --- a/basicswap/static/js/swaps_in_progress.js +++ b/basicswap/static/js/swaps_in_progress.js @@ -389,7 +389,7 @@ const createSwapTableRow = async (swap) => {
${swap.coin_from} @@ -398,7 +398,7 @@ const createSwapTableRow = async (swap) => { ${swap.coin_to} diff --git a/basicswap/templates/bid.html b/basicswap/templates/bid.html index 3bd66dc..71f967c 100644 --- a/basicswap/templates/bid.html +++ b/basicswap/templates/bid.html @@ -532,12 +532,12 @@
- {% endif %} - {% endif %} {% if data.can_abandon == true and not edit_bid %}
+ {% endif %} + {% endif %} {% endif %} {% if data.was_received and not edit_bid and data.can_accept_bid %}
diff --git a/basicswap/templates/bid_xmr.html b/basicswap/templates/bid_xmr.html index 8769961..f49b7ff 100644 --- a/basicswap/templates/bid_xmr.html +++ b/basicswap/templates/bid_xmr.html @@ -808,13 +808,13 @@
- {% endif %} - {% endif %} {% if data.can_abandon == true %}
{% endif %} + {% endif %} + {% endif %} {% if data.was_received and not edit_bid and data.can_accept_bid %}
diff --git a/basicswap/templates/offer.html b/basicswap/templates/offer.html index 5bece53..5114e53 100644 --- a/basicswap/templates/offer.html +++ b/basicswap/templates/offer.html @@ -846,6 +846,27 @@ function validateMaxAmount(input, maxAmount) { } function showConfirmModal() { + const bidAmountSendInput = document.getElementById('bid_amount_send'); + const bidAmountInput = document.getElementById('bid_amount'); + + let sendAmount = 0; + let receiveAmount = 0; + + if (bidAmountSendInput && bidAmountSendInput.value) { + sendAmount = parseFloat(bidAmountSendInput.value) || 0; + } + if (bidAmountInput && bidAmountInput.value) { + receiveAmount = parseFloat(bidAmountInput.value) || 0; + } + + if (sendAmount <= 0 && bidAmountSendInput && !bidAmountSendInput.disabled) { + return false; + } + + if (receiveAmount <= 0 && bidAmountInput && !bidAmountInput.disabled) { + return false; + } + updateModalValues(); const modal = document.getElementById('confirmModal'); if (modal) { diff --git a/basicswap/templates/offers.html b/basicswap/templates/offers.html index a5227ae..a39475e 100644 --- a/basicswap/templates/offers.html +++ b/basicswap/templates/offers.html @@ -199,97 +199,128 @@
-
+
-
-
-
-
-
-
-
-
- -
- {{ input_arrow_down_svg | safe }} - -
-
-
-

{{ arrow_right_svg | safe }}

-
-
- -
- {{ input_arrow_down_svg | safe }} - -
-
-
- {% if sent_offers %} -
-
-
- {{ input_arrow_down_svg | safe }} - -
-
-
- {% endif %} -
-
-
-
-
- {{ input_arrow_down_svg | safe }} - +
+
+
+
+
+ {{ input_arrow_down_svg | safe }} + + -
-
-
- +
+
+
+

{{ arrow_right_svg | safe }}

-
-
- +
+ {{ input_arrow_down_svg | safe }} + +
+ + {% if sent_offers %} +
+
+ {{ input_arrow_down_svg | safe }} + +
+
+ {% endif %} + +
+
+ {{ input_arrow_down_svg | safe }} + +
+
+ +
+
+ {{ input_arrow_down_svg | safe }} + +
+
+ +
+
+ +
+
+
+
+ +
+
+
@@ -297,7 +328,7 @@
-
+
diff --git a/basicswap/ui/page_offers.py b/basicswap/ui/page_offers.py index 7b7613f..2b77b2b 100644 --- a/basicswap/ui/page_offers.py +++ b/basicswap/ui/page_offers.py @@ -884,6 +884,7 @@ def page_offers(self, url_split, post_string, sent=False): "sort_dir": "desc", "sent_from": "any" if sent is False else "only", "active": "any", + "auto_accept_type": "any", } filter_prefix = "page_offers_sent" if sent else "page_offers" @@ -908,6 +909,16 @@ def page_offers(self, url_split, post_string, sent=False): sent_from = get_data_entry(form_data, "sent_from") ensure(sent_from in ["any", "only"], "Invalid sent filter") filters["sent_from"] = sent_from + if have_data_entry(form_data, "auto_accept_type"): + auto_accept_type = get_data_entry(form_data, "auto_accept_type") + ensure( + auto_accept_type in ["any", "0", "1", "2"], + "Invalid auto accept type filter", + ) + if auto_accept_type == "any": + filters["auto_accept_type"] = "any" + else: + filters["auto_accept_type"] = int(auto_accept_type) if have_data_entry(form_data, "active"): active_filter = get_data_entry(form_data, "active") ensure(