285 lines
11 KiB
Django/Jinja
285 lines
11 KiB
Django/Jinja
<style>
|
|
.timeline {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 3px;
|
|
user-select: none;
|
|
/* Prevent text selection during drag */
|
|
}
|
|
|
|
.hour-block {
|
|
width: 49px;
|
|
height: 70px;
|
|
/* Increased height to fit additional text */
|
|
border-radius: 5px;
|
|
text-align: center;
|
|
line-height: 1.2;
|
|
font-size: 0.9rem;
|
|
padding-top: 6px;
|
|
cursor: pointer;
|
|
user-select: none;
|
|
transition: background-color 0.2s ease-in-out;
|
|
margin: 1px;
|
|
}
|
|
|
|
.hour-block.selected {
|
|
outline: 2px solid #4584b8;
|
|
}
|
|
|
|
.papers {
|
|
font-size: 0.7rem;
|
|
margin-top: 2px;
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
const initialSchedule = {{ schedule | tojson }};
|
|
const totalVolume = {{ volume }};
|
|
</script>
|
|
|
|
<div x-data="scheduleManager(initialSchedule, totalVolume)" class="tab-pane active">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5>Scheduling Configuration</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
|
|
<!-- include flash messages template -->
|
|
{% include "partials/flash_messages.html.jinja" %}
|
|
|
|
<!-- Content -->
|
|
<div class="mb-3">
|
|
<h3>How it Works</h3>
|
|
<p class="text-muted mb-0">
|
|
This page allows you to configure the daily volume of papers to be
|
|
downloaded and the hourly download weights for the papers. The weights
|
|
determine how many papers will be downloaded during each hour of the day.
|
|
The total volume (<strong x-text="volume"></strong> papers/day) is split
|
|
across all hours based on their relative weights. Each weight controls the
|
|
proportion of papers downloaded during that hour. Click to select one or
|
|
more hours below. Then assign a weight to them using the input and apply
|
|
it. Color indicates relative intensity. The total daily volume will be
|
|
split proportionally across these weights.
|
|
<strong>Don't forget to submit the changes!</strong>
|
|
</p>
|
|
<h3>Example</h3>
|
|
<p class="text-muted mb-0">
|
|
If the total volume is <strong>240 papers</strong> and hours are
|
|
<strong>weighted as 1.0, 2.0, and 3.0</strong>, they will receive
|
|
<strong>40, 80, and 120 papers</strong> respectively.
|
|
</p>
|
|
</div>
|
|
|
|
<h2 class="mt-4">Volume</h2>
|
|
|
|
<div class="align-items-start flex-wrap gap-2">
|
|
<p class="text-muted">
|
|
The total volume of data to be downloaded each day is
|
|
<strong x-text="volume"></strong> papers.
|
|
</p>
|
|
<div class="d-flex align-items-center mb-3" x-data="{ volumeValue: volume }">
|
|
<div class="input-group w-50">
|
|
<label class="input-group-text">Papers per day:</label>
|
|
<input type="number" class="form-control" x-model="volumeValue" min="1" max="{{ max_volume }}"
|
|
required />
|
|
<button type="button" class="btn btn-primary" @click="updateVolume()">
|
|
Update Volume
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<h2 class="mt-4">Current Schedule</h2>
|
|
<form x-data id="scheduleForm">
|
|
<div class="timeline mb-3" @mouseup="endDrag()" @mouseleave="endDrag()">
|
|
<template x-for="hour in Object.keys(schedule)" :key="hour">
|
|
<div class="hour-block" :id="'hour-' + hour" :data-hour="hour" :style="getBackgroundStyle(hour)"
|
|
:class="{'selected': isSelected(hour)}" @mousedown="startDrag($event, hour)"
|
|
@mouseover="dragSelect(hour)">
|
|
<div><strong x-text="formatHour(hour)"></strong></div>
|
|
<div class="weight"><span x-text="schedule[hour]"></span></div>
|
|
<div class="papers">
|
|
<span x-text="getPapersPerHour(hour)"></span> p.
|
|
</div>
|
|
<input type="hidden" :name="'hour_' + hour" :value="schedule[hour]" />
|
|
</div>
|
|
</template>
|
|
</div>
|
|
|
|
<div class="input-group mb-4 w-50">
|
|
<label class="input-group-text">Set Weight:</label>
|
|
<input type="number" step="0.1" min="0" max="5" x-model="newWeight" class="form-control" />
|
|
<button type="button" class="btn btn-outline-primary" @click="applyWeight()">
|
|
Apply to Selected
|
|
</button>
|
|
</div>
|
|
|
|
<div class="d-flex justify-content-between">
|
|
<a href="{{ url_for('config.general') }}" class="btn btn-outline-secondary">⬅ Back</a>
|
|
<button type="button" class="btn btn-success" @click="saveSchedule()">💾 Save Schedule</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
function scheduleManager(initial, volume) {
|
|
return {
|
|
schedule: { ...initial },
|
|
volume: volume,
|
|
selectedHours: [],
|
|
newWeight: 1.0,
|
|
volumeValue: volume,
|
|
isDragging: false,
|
|
dragOperation: null,
|
|
|
|
formatHour(h) {
|
|
return String(h).padStart(2, "0") + ":00";
|
|
},
|
|
|
|
updateVolume() {
|
|
fetch('{{ url_for('config.api_update_config') }}', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
volume: this.volumeValue
|
|
})
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
this.volume = parseFloat(this.volumeValue);
|
|
showFlashMessage('Volume updated successfully!', 'success');
|
|
} else {
|
|
showFlashMessage(data.updates?.[0]?.message || 'Error updating volume', 'error');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error:', error);
|
|
showFlashMessage('Network error occurred', 'error');
|
|
});
|
|
},
|
|
|
|
getBackgroundStyle(hour) {
|
|
const weight = parseFloat(this.schedule[hour]);
|
|
const maxWeight = 2.5; // You can adjust this
|
|
|
|
// Normalize weight (0.0 to 1.0)
|
|
const t = Math.min(weight / maxWeight, 1.0);
|
|
|
|
// Interpolate HSL lightness: 95% (light) to 30% (dark)
|
|
const lightness = 95 - t * 65; // 95 → 30
|
|
const backgroundColor = `hsl(210, 10%, ${lightness}%)`; // soft gray-blue palette
|
|
|
|
const textColor = t > 0.65 ? "white" : "black"; // adaptive text color
|
|
|
|
return {
|
|
backgroundColor,
|
|
color: textColor,
|
|
};
|
|
},
|
|
|
|
getBackgroundStyleFromValue(value) {
|
|
const weight = parseFloat(value);
|
|
const maxWeight = 2.5; // You can adjust this
|
|
|
|
// Normalize weight (0.0 to 1.0)
|
|
const t = Math.min(weight / maxWeight, 1.0);
|
|
|
|
// Interpolate HSL lightness: 95% (light) to 30% (dark)
|
|
const lightness = 95 - t * 65; // 95 → 30
|
|
const backgroundColor = `hsl(210, 10%, ${lightness}%)`; // soft gray-blue palette
|
|
|
|
const textColor = t > 0.65 ? "white" : "black"; // adaptive text color
|
|
|
|
return {
|
|
backgroundColor,
|
|
color: textColor,
|
|
};
|
|
},
|
|
|
|
startDrag(event, hour) {
|
|
event.preventDefault();
|
|
this.isDragging = true;
|
|
this.dragOperation = this.isSelected(hour) ? "remove" : "add";
|
|
this.toggleSelect(hour);
|
|
},
|
|
|
|
dragSelect(hour) {
|
|
if (!this.isDragging) return;
|
|
const selected = this.isSelected(hour);
|
|
if (this.dragOperation === "add" && !selected) {
|
|
this.selectedHours.push(hour);
|
|
} else if (this.dragOperation === "remove" && selected) {
|
|
this.selectedHours = this.selectedHours.filter((h) => h !== hour);
|
|
}
|
|
},
|
|
|
|
endDrag() {
|
|
this.isDragging = false;
|
|
},
|
|
|
|
toggleSelect(hour) {
|
|
if (this.isSelected(hour)) {
|
|
this.selectedHours = this.selectedHours.filter((h) => h !== hour);
|
|
} else {
|
|
this.selectedHours.push(hour);
|
|
}
|
|
},
|
|
|
|
isSelected(hour) {
|
|
return this.selectedHours.includes(hour);
|
|
},
|
|
|
|
applyWeight() {
|
|
this.selectedHours.forEach((hour) => {
|
|
this.schedule[hour] = parseFloat(this.newWeight).toFixed(1);
|
|
});
|
|
this.selectedHours = [];
|
|
},
|
|
|
|
getTotalWeight() {
|
|
return Object.values(this.schedule).reduce(
|
|
(sum, w) => sum + parseFloat(w),
|
|
0
|
|
);
|
|
},
|
|
|
|
getPapersPerHour(hour) {
|
|
const total = this.getTotalWeight();
|
|
if (total === 0) return 0;
|
|
return (
|
|
(parseFloat(this.schedule[hour]) / total) *
|
|
this.volume
|
|
).toFixed(1);
|
|
},
|
|
|
|
saveSchedule() {
|
|
fetch('{{ url_for('config.api_update_config') }}', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
schedule: this.schedule
|
|
})
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
showFlashMessage('Schedule updated successfully!', 'success');
|
|
} else {
|
|
showFlashMessage(data.updates?.[0]?.message || 'Error updating schedule', 'error');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error:', error);
|
|
showFlashMessage('Network error occurred', 'error');
|
|
});
|
|
}
|
|
};
|
|
}
|
|
</script> |