divan/GanjooRazor/Areas/User/Pages/Editor.cshtml
Hamid Reza Mohammadi 3493225dc5 people tags #387
2026-10-03 21:16:42 +03:30

2933 lines
152 KiB
Plaintext
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

@page
@model GanjooRazor.Areas.User.Pages.EditorModel
@using DNTPersianUtils.Core
@{
Layout = "_UserPanelLayout";
ViewData["Title"] = "ویرایشگر";
await GanjooRazor.Utils.GanjoorSessionChecker.ApplyPermissionsToViewData(Request, Response, ViewData);
}
<div class="up-page-header">
<h1>@ViewData["Title"]</h1>
</div>
<script>
$(document).ready(function () {
$('[contenteditable]').keypress(function (e) { return e.which != 13; });
});
</script>
<script>
function resetVersePositions(verseCount) {
for (var i = 1; i <= verseCount; i++) {
var newVersePosition = 'versepostion-' + i.toString();
document.getElementById(newVersePosition).value = i % 2 == 1 ? 'Right' : 'Left';
}
upToast('انجام شد.');
}
function tarkib3VersePositions(verseCount) {
var index = 1;
for (var i = 1; i <= verseCount; i++) {
var newVersePosition = 'versepostion-' + i.toString();
document.getElementById(newVersePosition).value = index == 1 ? 'Right' : index == 2 ? 'Left' : 'CenteredVerse1';
index++;
if(index == 4){
index = 1;
}
}
upToast('انجام شد.');
}
function tarkib4VersePositions(verseCount) {
var index = 1;
for (var i = 1; i <= verseCount; i++) {
var newVersePosition = 'versepostion-' + i.toString();
document.getElementById(newVersePosition).value = index == 1 ? 'Right' : index == 2 ? 'Left' : index == 3 ? 'CenteredVerse1' : 'CenteredVerse2';
index++;
if(index == 5){
index = 1;
}
}
upToast('انجام شد.');
}
function tarkib5VersePositions(verseCount) {
var index = 1;
for (var i = 1; i <= verseCount; i++) {
var newVersePosition = 'versepostion-' + i.toString();
document.getElementById(newVersePosition).value = index == 1 || index == 3 ? 'Right' : index == 2 || index == 4 ? 'Left' : 'CenteredVerse1';
index++;
if(index == 6){
index = 1;
}
}
upToast('انجام شد.');
}
function tarkib6VersePositions(verseCount) {
var index = 1;
for (var i = 1; i <= verseCount; i++) {
var newVersePosition = 'versepostion-' + i.toString();
document.getElementById(newVersePosition).value = index == 1 || index == 3 ? 'Right' : index == 2 || index == 4 ? 'Left' : index == 5 ? 'CenteredVerse1' : 'CenteredVerse2';
index++;
if(index == 7){
index = 1;
}
}
upToast('انجام شد.');
}
function tarkib7VersePositions(verseCount) {
var index = 1;
for (var i = 1; i <= verseCount; i++) {
var newVersePosition = 'versepostion-' + i.toString();
document.getElementById(newVersePosition).value = index == 1 || index == 3 || index == 5 ? 'Right' : index == 2 || index == 4 || index == 6 ? 'Left' : 'CenteredVerse1';
index++;
if(index == 8){
index = 1;
}
}
upToast('انجام شد.');
}
function tarkib8VersePositions(verseCount) {
var index = 1;
for (var i = 1; i <= verseCount; i++) {
var newVersePosition = 'versepostion-' + i.toString();
document.getElementById(newVersePosition).value = index == 1 || index == 3 || index == 5 ? 'Right' : index == 2 || index == 4 || index == 6 ? 'Left' : index == 7 ? 'CenteredVerse1' : 'CenteredVerse2';
index++;
if(index == 9){
index = 1;
}
}
upToast('انجام شد.');
}
function toggleMarkedForDelete(vOrder) {
var checkbox = document.getElementById('marked-for-delete-' + vOrder.toString());
var verseText = document.getElementById('id-' + vOrder.toString());
if (!checkbox || !verseText) return;
if (checkbox.checked) {
verseText.classList.add('up-editable--marked-for-delete');
} else {
verseText.classList.remove('up-editable--marked-for-delete');
}
}
// ---------- geo/date tag AND person (شخصیت) tag suggestions ----------
// Both kinds are staged client-side (until the main save) in this ONE combined array, and both
// are sent to the server as one flat GanjoorPoemGeoDateTagCorrection[] (pcs.geoDateTags) - that
// part of the data model is shared, on purpose (see the doc comment on
// GanjoorPoemGeoDateTagCorrection.SuggestedPersonGraphJson). But a geo/date tag and a person tag
// mean unrelated things to a reader ("this couplet refers to this place/time" vs "this couplet
// refers to this person"), so the UI never mixes them in one row: every staged/loaded item here
// carries a client-only "_uiKind" field ('geo' or 'person', added by _getSuggestionUiKind for
// anything that doesn't already have it - e.g. a draft loaded from the server) that the two
// render functions (renderPendingGeoDateTags / renderPendingPersonTags) filter on, and the two
// "add a tag" cards each read/write only their own half of the form.
var geoDateTagSuggestions = @Html.Raw(Model.MyLastEditGeoDateTagsJson);
var geoDateTagCoupletTexts = @Html.Raw(Newtonsoft.Json.JsonConvert.SerializeObject(Model.Couplets.ToDictionary(c => c.Item1.ToString(), c => c.Item2)));
// full existing-location catalog ({id, name, latitude, longitude}[]), used to warn about a
// new-location suggestion that's actually already catalogued - see _confirmNewLocationSuggestion
var allGeoLocations = @Html.Raw(Model.AllLocationsJson);
// full existing-people catalog ({id, name, birthYearInLHijri, deathYearInLHijri}[]), used both
// to pick an existing person for a tag and as the "other side" of a new person's relatives
var allGeoPeople = @Html.Raw(Model.AllPeopleJson);
// {id, personId}[] for this poem's already-approved geo/date/person tags - used only to work out
// the _uiKind of a "delete this existing tag" suggestion loaded from a saved-but-unsubmitted
// draft, which otherwise carries nothing but the id being deleted
var allExistingGeoDateTags = @Html.Raw(Newtonsoft.Json.JsonConvert.SerializeObject(
(Model.PoemGeoDateTags ?? Array.Empty<RMuseum.Models.Ganjoor.PoemGeoDateTag>()).Select(t => new { id = t.Id, personId = t.PersonId })));
// Works out whether a staged/loaded suggestion is a geo/date tag or a person (شخصیت) tag, for the
// two render functions to filter on. New items always set _uiKind themselves when pushed; this
// is only needed for items loaded from Model.MyLastEditGeoDateTagsJson (an unsubmitted draft
// from a previous visit), which predate the split and never carry it.
function _getSuggestionUiKind(s) {
if (s._uiKind) return s._uiKind;
if (s.markForDelete) {
var existing = allExistingGeoDateTags.filter(function (t) { return t.id === s.existingTagId; })[0];
return (existing && existing.personId) ? 'person' : 'geo';
}
return (s.personId != null || s.suggestedPersonGraphJson) ? 'person' : 'geo';
}
$(document).ready(function () {
renderPendingGeoDateTags();
renderPendingPersonTags();
setupLocationAutocomplete(
document.getElementById('geotag-location-search'),
document.getElementById('geotag-location-select'),
document.getElementById('geotag-location-results'),
allGeoLocations,
function (loc) {
// picking an existing location (or clearing the field) means whatever was
// typed into the "new location" panel no longer applies to this suggestion
var widget = document.getElementById('new-location-widget');
if (loc != null && widget.style.display !== 'none') {
toggleNewLocationWidget();
}
},
function (typedText) {
var widget = document.getElementById('new-location-widget');
if (widget.style.display === 'none' || widget.style.display === '') {
toggleNewLocationWidget();
}
document.getElementById('geotag-new-location-name').value = typedText;
}
);
var directAddLocationSearch = document.getElementById('location-search');
if (directAddLocationSearch != null) {
// admin-only direct-add row (bypasses the suggestion/moderation flow entirely) -
// no "new location" concept here, so an unmatched search just has nothing to pick
setupLocationAutocomplete(
directAddLocationSearch,
document.getElementById('location'),
document.getElementById('location-results'),
allGeoLocations,
null,
function (typedText) {
upToast('این مکان در فهرست نیست. برای افزودن مکان جدید از «مکان‌ها» در پایین این جدول استفاده کنید.', 'error');
}
);
}
setupPersonAutocomplete(
document.getElementById('geotag-person-search'),
document.getElementById('geotag-person-select'),
document.getElementById('geotag-person-results'),
allGeoPeople,
function (person) {
// picking an existing person (or clearing the field) means whatever was
// typed into the "new person" panel no longer applies to this suggestion
var widget = document.getElementById('new-person-widget');
if (person != null && widget.style.display !== 'none') {
toggleNewPersonWidget();
}
},
function (typedText) {
var widget = document.getElementById('new-person-widget');
if (widget.style.display === 'none' || widget.style.display === '') {
toggleNewPersonWidget();
}
document.getElementById('geotag-new-person-name').value = typedText;
}
);
});
var geoDateTagMap = null;
var geoDateTagMarker = null;
var personRelativeRowCounter = 0; // ever-increasing, so removed rows never clash with new ones
function toggleNewPersonWidget() {
var widget = document.getElementById('new-person-widget');
var isHidden = widget.style.display === 'none' || widget.style.display === '';
widget.style.display = isHidden ? 'block' : 'none';
}
// adds one "relative or affiliation" row to the new-person panel. The other side of the
// relation must already exist (see the hint text above the button) - nesting another
// brand-new person inside this one's relatives is a rare-enough case that it's left for a
// later pass; the backend already supports it via relatedPeople, just not from this form yet.
function addPersonRelativeRow() {
var idx = personRelativeRowCounter++;
var row = document.createElement('div');
row.id = 'person-relative-row-' + idx;
row.className = 'up-card';
row.style.marginTop = '8px';
row.innerHTML =
'<div class="up-field-row">' +
'<div class="up-field">' +
'<label>نوع رابطه</label>' +
'<select id="person-relative-kind-' + idx + '" onchange="togglePersonRelativeKind(' + idx + ')">' +
'<option value="family">خویشاوندی</option>' +
'<option value="affiliation">غیرخویشاوندی (مثل وزارت، مشاورت)</option>' +
'</select>' +
'</div>' +
'<div class="up-field" id="person-relative-familytype-wrap-' + idx + '">' +
'<label>نوع خویشاوندی</label>' +
'<select id="person-relative-familytype-' + idx + '" onchange="togglePersonRelativeDegreeHint(' + idx + ')">' +
'<option value="Parent">پدر/مادر ↔ فرزند</option>' +
'<option value="Sibling">خواهر/برادر</option>' +
'<option value="Spouse">همسر</option>' +
'<option value="Ancestor">نیا ↔ نواده (با فاصلهٔ نسل نامعلوم یا مشخص)</option>' +
'</select>' +
'</div>' +
'<div class="up-field" id="person-relative-affiliationtype-wrap-' + idx + '" style="display:none;">' +
'<label>نوع وابستگی</label>' +
'<select id="person-relative-affiliationtype-' + idx + '" onchange="updatePersonRelativeHint(' + idx + ')">' +
'<option value="Minister">وزیر</option>' +
'<option value="Advisor">مشاور</option>' +
'<option value="Courtier">درباری</option>' +
'<option value="Patron">حامی</option>' +
'<option value="Ally">متحد</option>' +
'<option value="Rival">رقیب</option>' +
'<option value="Panegyrized">مدح‌گو (طرف اول، طرف دوم را مدح کرده)</option>' +
'<option value="Satirized">هجوگو (طرف اول، طرف دوم را هجو کرده)</option>' +
'<option value="MilitaryCommander">سردار</option>' +
'<option value="Champion">پهلوان</option>' +
'<option value="Contemporary">هم‌عصر (بدون نسبت دیگر)</option>' +
'<option value="Other">سایر</option>' +
'</select>' +
'</div>' +
'<div class="up-field" id="person-relative-degreehint-wrap-' + idx + '" style="display:none;">' +
'<label>تعداد نسل (اختیاری، فقط اگر دقیقاً معلوم است)</label>' +
'<input type="text" id="person-relative-degreehint-' + idx + '" placeholder="مثلاً ۲ برای پدربزرگ/مادربزرگ" />' +
'</div>' +
'</div>' +
'<div class="up-field-row" id="person-relative-direction-wrap-' + idx + '">' +
'<div class="up-field">' +
'<label>جهت رابطه</label>' +
// "otherFirst" (the already-existing person is the parent side) is listed first
// and pre-selected, since a row starts out on the "family" kind and the usual
// workflow there is building a tree top-down - the ancestor/senior side already
// exists, and this new person being created is their child. Defaulting to
// "newFirst" instead silently assumed the opposite and was an easy, hard-to-notice
// way to enter a relation backwards (e.g. recording a son as his own father's
// parent). Non-family affiliations default the other way around - see
// togglePersonRelativeKind(), which flips this select's value on kind change.
'<select id="person-relative-direction-' + idx + '" onchange="updatePersonRelativeHint(' + idx + ')">' +
'<option value="otherFirst" selected="selected">نامبردۀ دیگر، طرف اول رابطه است (مثلاً پدر/وزیر این نامبردۀ جدید)</option>' +
'<option value="newFirst">این نامبردۀ جدید، طرف اول رابطه است (مثلاً پدر/وزیر طرف دوم)</option>' +
'</select>' +
'</div>' +
'</div>' +
'<div class="up-field-row">' +
'<div class="up-field">' +
'<label>نامبردۀ دیگر (باید از قبل در فهرست موجود باشد)</label>' +
'<div class="up-autocomplete-wrap">' +
'<input type="text" id="person-relative-person-search-' + idx + '" autocomplete="off" placeholder="نام نامبرده را تایپ کنید..." oninput="updatePersonRelativeHint(' + idx + ')" />' +
'<input type="hidden" id="person-relative-person-id-' + idx + '" />' +
'<div id="person-relative-person-results-' + idx + '" class="up-autocomplete-results" style="display:none;"></div>' +
'</div>' +
'</div>' +
'<div class="up-field">' +
'<label>یادداشت (اختیاری)</label>' +
'<input type="text" id="person-relative-note-' + idx + '" />' +
'</div>' +
'<a role="button" class="up-btn up-btn--sm up-btn--ghost" onclick="removePersonRelativeRow(' + idx + ')">✕ حذف این رابطه</a>' +
'</div>' +
// live "x پدر/مادر y است."-style readback of whatever is currently selected, so the
// user can confirm the direction is right before submitting, instead of having to
// mentally translate the kind/type/direction combo themselves every time
'<p class="up-hint" id="person-relative-hint-' + idx + '"></p>';
document.getElementById('geotag-person-relatives-list').appendChild(row);
setupPersonAutocomplete(
document.getElementById('person-relative-person-search-' + idx),
document.getElementById('person-relative-person-id-' + idx),
document.getElementById('person-relative-person-results-' + idx),
allGeoPeople,
function () {
updatePersonRelativeHint(idx);
},
function (typedText) {
upToast('این نامبرده در فهرست نیست. طرف دیگر یک رابطه باید از قبل تعریف‌شده باشد؛ برای دو نامبردۀ تازه، ابتدا این فرم را برای یکی از آن‌ها به‌صورت جدا تکمیل و ثبت کنید.', 'error');
}
);
updatePersonRelativeHint(idx);
}
function removePersonRelativeRow(idx) {
var row = document.getElementById('person-relative-row-' + idx);
if (row) row.remove();
}
function togglePersonRelativeKind(idx) {
var kind = $('#person-relative-kind-' + idx).val();
var isFamily = kind === 'family';
$('#person-relative-familytype-wrap-' + idx).toggle(isFamily);
$('#person-relative-affiliationtype-wrap-' + idx).toggle(!isFamily);
togglePersonRelativeDegreeHint(idx);
// Family relations default to "the already-existing person is the first/senior side" - the
// usual top-down tree-building flow, where the parent is already in the system and this new
// person is their child. Non-family affiliations run the other way around in the common case:
// the already-existing person is typically the senior/more prominent figure (a king, a
// patron) already in the system, and the new person being added is the one standing in the
// subordinate affiliation role toward them (their minister, their advisor) - so for
// affiliations the new person should default to being the first side instead.
$('#person-relative-direction-' + idx).val(isFamily ? 'otherFirst' : 'newFirst');
updatePersonRelativeHint(idx);
}
function togglePersonRelativeDegreeHint(idx) {
var kind = $('#person-relative-kind-' + idx).val();
var isAncestor = kind === 'family' && $('#person-relative-familytype-' + idx).val() === 'Ancestor';
$('#person-relative-degreehint-wrap-' + idx).toggle(isAncestor);
updatePersonRelativeHint(idx);
}
// one sentence-building function per relation type (family kind) / affiliation type, so the
// "جهت رابطه" readback hint below each row can show something like "x پدر/مادر y است." using
// whichever two names are currently in play - matches the wording used elsewhere for the same
// types (PersonWindow's role words, ReviewPersonRelationEdits' labels)
var PERSON_RELATIVE_HINT_TEMPLATES = {
family: {
Parent: function (p1, p2) { return p1 + ' پدر/مادر ' + p2 + ' است.'; },
Sibling: function (p1, p2) { return p1 + ' و ' + p2 + ' خواهر/برادر هم هستند.'; },
Spouse: function (p1, p2) { return p1 + ' و ' + p2 + ' همسر هم هستند.'; },
Ancestor: function (p1, p2) { return p1 + ' نیای ' + p2 + ' است.'; }
},
affiliation: {
Minister: function (p1, p2) { return p1 + ' وزیر ' + p2 + ' است.'; },
Advisor: function (p1, p2) { return p1 + ' مشاور ' + p2 + ' است.'; },
Courtier: function (p1, p2) { return p1 + ' درباری ' + p2 + ' است.'; },
Servant: function (p1, p2) { return p1 + ' خدمتگزار ' + p2 + ' است.'; },
Patron: function (p1, p2) { return p1 + ' حامی ' + p2 + ' است.'; },
Successor: function (p1, p2) { return p1 + ' جانشین ' + p2 + ' است.'; },
Panegyrized: function (p1, p2) { return p1 + ' ' + p2 + ' را مدح کرده است.'; },
Satirized: function (p1, p2) { return p1 + ' ' + p2 + ' را هجو کرده است.'; },
Ally: function (p1, p2) { return p1 + ' و ' + p2 + ' متحد هم هستند.'; },
Rival: function (p1, p2) { return p1 + ' و ' + p2 + ' رقیب هم هستند.'; },
Companion: function (p1, p2) { return p1 + ' و ' + p2 + ' همراه هم هستند.'; },
MilitaryCommander: function (p1, p2) { return p1 + ' سردار ' + p2 + ' است.'; },
Champion: function (p1, p2) { return p1 + ' پهلوان ' + p2 + ' است.'; },
Contemporary: function (p1, p2) { return p1 + ' و ' + p2 + ' هم‌عصر هم هستند.'; },
Other: function (p1, p2) { return p1 + ' با ' + p2 + ' نسبتی دارد (طبق یادداشت).'; }
}
};
function updatePersonRelativeHint(idx) {
var hintEl = document.getElementById('person-relative-hint-' + idx);
if (!hintEl) return;
var kind = $('#person-relative-kind-' + idx).val();
var type = kind === 'family'
? $('#person-relative-familytype-' + idx).val()
: $('#person-relative-affiliationtype-' + idx).val();
var direction = $('#person-relative-direction-' + idx).val();
var otherName = ($('#person-relative-person-search-' + idx).val() || '').trim() || 'نامبردۀ دیگر';
var newName = ($('#geotag-new-person-name').val() || '').trim() || 'این نامبردۀ جدید';
var person1Name = direction === 'otherFirst' ? otherName : newName;
var person2Name = direction === 'otherFirst' ? newName : otherName;
var build = (PERSON_RELATIVE_HINT_TEMPLATES[kind] || {})[type];
hintEl.textContent = build ? build(person1Name, person2Name) : '';
}
// the new person's own name field is shared by every relative row, so typing into it has to
// refresh all of them, not just whichever row happens to be focused
function updateAllPersonRelativeHints() {
$('[id^="person-relative-hint-"]').each(function () {
updatePersonRelativeHint(this.id.replace('person-relative-hint-', ''));
});
}
function toggleNewLocationWidget() {
var widget = document.getElementById('new-location-widget');
var isHidden = widget.style.display === 'none' || widget.style.display === '';
widget.style.display = isHidden ? 'block' : 'none';
// the map is a secondary way to set the coordinates (typing them in from Wikipedia/GeoHack,
// per the hint text, is the primary one) but it's shown right away, not behind another
// click, since a hidden-behind-a-link map is easy to miss entirely
if (isHidden && geoDateTagMap == null) {
geoDateTagMap = L.map('geotag-map').setView([32.4279, 53.6880], 5); // Iran-centered default
L.tileLayer.provider('OpenStreetMap').addTo(geoDateTagMap);
geoDateTagMap.on('click', function (e) {
if (geoDateTagMarker != null) {
geoDateTagMap.removeLayer(geoDateTagMarker);
}
geoDateTagMarker = L.marker(e.latlng).addTo(geoDateTagMap);
document.getElementById('geotag-new-location-lat').value = e.latlng.lat;
document.getElementById('geotag-new-location-lng').value = e.latlng.lng;
});
}
if (isHidden && geoDateTagMap != null) {
// the map was created while its container was display:none (or is just being
// re-shown), so Leaflet needs a nudge to recalculate its size once it's visible
setTimeout(function () { geoDateTagMap.invalidateSize(); }, 100);
}
}
// moves the embedded map (and its marker) to the typed-in lat/lng, so the user can check they
// copied the right numbers before adding the suggestion, without leaving the page - this used
// to open Google Maps in a new tab, but that made the two ways of setting a location (typing
// coordinates vs. clicking the map) feel disconnected: clicking the map updated the fields, but
// typing into the fields never showed up on the map. now both directions go through the same map.
function viewNewLocationOnMap() {
var latStr = document.getElementById('geotag-new-location-lat').value;
var lngStr = document.getElementById('geotag-new-location-lng').value;
if (!latStr || !lngStr) {
upToast('ابتدا عرض و طول جغرافیایی را وارد کنید.', 'error');
return;
}
var lat = parseFloat(latStr);
var lng = parseFloat(lngStr);
if (isNaN(lat) || isNaN(lng) || lat < -90 || lat > 90 || lng < -180 || lng > 180) {
upToast('عرض جغرافیایی باید بین ۹۰- تا ۹۰ و طول جغرافیایی باید بین ۱۸۰- تا ۱۸۰ باشد.', 'error');
return;
}
// the map is normally created when the "new location" widget is opened, but guard here
// too in case this ever gets wired to a control outside that widget
if (geoDateTagMap == null) {
geoDateTagMap = L.map('geotag-map').setView([lat, lng], 5);
L.tileLayer.provider('OpenStreetMap').addTo(geoDateTagMap);
geoDateTagMap.on('click', function (e) {
if (geoDateTagMarker != null) {
geoDateTagMap.removeLayer(geoDateTagMarker);
}
geoDateTagMarker = L.marker(e.latlng).addTo(geoDateTagMap);
document.getElementById('geotag-new-location-lat').value = e.latlng.lat;
document.getElementById('geotag-new-location-lng').value = e.latlng.lng;
});
}
else {
geoDateTagMap.invalidateSize();
}
if (geoDateTagMarker != null) {
geoDateTagMap.removeLayer(geoDateTagMarker);
}
geoDateTagMarker = L.marker([lat, lng]).addTo(geoDateTagMap);
geoDateTagMap.setView([lat, lng], 12);
}
// great-circle distance in km - just for catching obviously-duplicate locations, not
// geodesy-grade precision
function _geoDistanceKm(lat1, lng1, lat2, lng2) {
var R = 6371;
var dLat = (lat2 - lat1) * Math.PI / 180;
var dLng = (lng2 - lng1) * Math.PI / 180;
var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
Math.sin(dLng / 2) * Math.sin(dLng / 2);
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}
// For a brand-new location suggestion, checks it against the full existing-location catalog
// (allGeoLocations) before it's staged. Two cases:
// - near-identical coordinates to an existing location: almost certainly the same real place,
// typed in again instead of picked from the dropdown - blocked outright, no point asking.
// - same name as an existing location but meaningfully different coordinates: could be a real
// second place that happens to share a name, or could be a coordinate mistake - asks the
// user to explicitly confirm rather than guessing either way.
// Returns true if the tag should be added as-is.
async function _confirmNewLocationSuggestion(tag) {
if (!tag || tag.locationId != null || !tag.suggestedLocationName) {
return true; // an existing/selected location, or no location at all - nothing to check
}
var NEAR_DUPLICATE_KM = 5; // close enough that it's almost certainly the same real place
var trimmedName = tag.suggestedLocationName.trim();
for (var i = 0; i < allGeoLocations.length; i++) {
var existing = allGeoLocations[i];
var distanceKm = _geoDistanceKm(tag.suggestedLatitude, tag.suggestedLongitude, existing.latitude, existing.longitude);
if (distanceKm <= NEAR_DUPLICATE_KM) {
upToast('این مکان با نام «' + existing.name + '» و مختصات بسیار نزدیک از قبل ثبت شده است. لطفاً به‌جای پیشنهاد مکان جدید، همان را از فهرست «مکان» انتخاب کنید.', 'error');
return false;
}
}
var sameNameOtherPlaces = allGeoLocations.filter(function (existing) {
return existing.name && existing.name.trim().toLowerCase() === trimmedName.toLowerCase();
});
if (sameNameOtherPlaces.length > 0) {
var distances = sameNameOtherPlaces.map(function (existing) {
return Math.round(_geoDistanceKm(tag.suggestedLatitude, tag.suggestedLongitude, existing.latitude, existing.longitude));
});
var ok = await upConfirm(
'مکانی با نام «' + trimmedName + '» از قبل با مختصات دیگری در فهرست ثبت شده است (فاصله حدود ' + distances.join('، ') + ' کیلومتر). ' +
'آیا مطمئنید این مکان دیگری با همین نام است و اشتباهی در مختصات پیش نیامده؟',
{ okText: 'بله، مکان دیگری است', cancelText: 'انصراف' }
);
if (!ok) {
return false;
}
}
return true;
}
// Builds the SuggestedPersonGraphJson payload (see GanjoorPoemGeoDateTagCorrection's doc
// comment for the exact shape) from the "new person" panel and its relatives-list rows.
// Returns null (with an error toast) if the panel is open but something required is missing;
// the caller is expected to have already checked the panel is actually meant to be used.
function buildPersonGraphJson() {
var name = ($('#geotag-new-person-name').val() || '').trim();
if (name == '') {
upToast('لطفاً نام نامبردۀ جدید را وارد کنید.', 'error');
return null;
}
// birth/death year, "date is documented" and birth/death location are left out of this
// form for now (no real usage of them yet elsewhere) - the shape below still carries all
// of PersonGraphNode's fields, just with those ones always null/false, same as any other
// field this form doesn't offer a control for.
var person = {
localKey: 'new',
existingPersonId: null,
name: name,
description: $('#geotag-new-person-description').val() || null,
wikiUrl: $('#geotag-new-person-wikiurl').val() || null,
birthYearInLHijri: null,
deathYearInLHijri: null,
validBirthDate: false,
validDeathDate: false,
birthLocationId: null,
deathLocationId: null,
familyTreeCaption: $('#geotag-new-person-familytreecaption').val() || null,
importance: $('#geotag-new-person-importance').val(),
gender: $('#geotag-new-person-gender').val()
};
var relatedPeople = [];
var relatedPeopleByPersonId = {}; // existingPersonId -> localKey, so a person picked in two rows isn't duplicated
var relations = [];
var rows = $('#geotag-person-relatives-list > div');
for (var r = 0; r < rows.length; r++) {
var idx = rows[r].id.replace('person-relative-row-', '');
var otherPersonId = $('#person-relative-person-id-' + idx).val();
if (!otherPersonId) {
upToast('برای یکی از روابط افزوده‌شده، نامبردۀ دیگر انتخاب نشده است.', 'error');
return null;
}
otherPersonId = parseInt(otherPersonId);
var otherLocalKey = relatedPeopleByPersonId[otherPersonId];
if (!otherLocalKey) {
otherLocalKey = 'existing_' + otherPersonId;
relatedPeopleByPersonId[otherPersonId] = otherLocalKey;
relatedPeople.push({ localKey: otherLocalKey, existingPersonId: otherPersonId });
}
var kind = $('#person-relative-kind-' + idx).val();
var direction = $('#person-relative-direction-' + idx).val();
var person1 = direction === 'otherFirst' ? otherLocalKey : 'new';
var person2 = direction === 'otherFirst' ? 'new' : otherLocalKey;
var note = $('#person-relative-note-' + idx).val() || null;
if (kind === 'affiliation') {
relations.push({
kind: 'affiliation',
person1: person1,
person2: person2,
affiliationType: $('#person-relative-affiliationtype-' + idx).val(),
note: note
});
} else {
var relationType = $('#person-relative-familytype-' + idx).val();
var degreeHintStr = $('#person-relative-degreehint-' + idx).val();
relations.push({
kind: 'family',
person1: person1,
person2: person2,
relationType: relationType,
degreeHint: (relationType === 'Ancestor' && degreeHintStr) ? parseInt(degreeHintStr) : null,
note: note
});
}
}
return JSON.stringify({ person: person, relatedPeople: relatedPeople, relations: relations });
}
function renderPendingGeoDateTags() {
var container = document.getElementById('pending-geotag-suggestions');
var rows = [];
for (var i = 0; i < geoDateTagSuggestions.length; i++) {
if (_getSuggestionUiKind(geoDateTagSuggestions[i]) === 'geo') rows.push(i);
}
if (rows.length == 0) {
container.innerHTML = '';
return;
}
var html = '<table><tr><th>وضعیت</th><th>بیت</th><th>مکان</th><th>سال</th><th>ماه</th><th>روز</th><th>#</th></tr>';
for (var r = 0; r < rows.length; r++) {
var i = rows[r];
var s = geoDateTagSuggestions[i];
var status = s.markForDelete ? '🗑️ درخواست حذف' : '➕ پیشنهاد جدید';
var coupletText = (s.coupletIndex != null && geoDateTagCoupletTexts[s.coupletIndex]) ? geoDateTagCoupletTexts[s.coupletIndex] : '— کل شعر —';
var locationText = s.markForDelete ? '' : (s.locationId ? ((allGeoLocations.filter(function (l) { return l.id === s.locationId; })[0] || {}).name || ('مکان #' + s.locationId)) : (s.suggestedLocationName || ''));
html += '<tr><td>' + status + '</td><td>' + coupletText + '</td><td>' + locationText + '</td><td>' + (s.lunarYear || '') + '</td><td>' + (s.lunarMonth || '') + '</td><td>' + (s.lunarDay || '') + '</td>' +
'<td><a role="button" class="up-btn up-btn--sm up-btn--ghost" onclick="removeGeoDateTagSuggestion(' + i + ')">✕ حذف از فهرست</a></td></tr>';
}
html += '</table>';
container.innerHTML = html;
}
// same purpose/shape as renderPendingGeoDateTags above, but for the person(شخصیت) tag section -
// kept as a fully separate function (rather than one function with a "kind" switch inside) so
// each half of the split UI is easy to read/change on its own
function renderPendingPersonTags() {
var container = document.getElementById('pending-persontag-suggestions');
var rows = [];
for (var i = 0; i < geoDateTagSuggestions.length; i++) {
if (_getSuggestionUiKind(geoDateTagSuggestions[i]) === 'person') rows.push(i);
}
if (rows.length == 0) {
container.innerHTML = '';
return;
}
var html = '<table><tr><th>وضعیت</th><th>بیت</th><th>نامبرده</th><th>#</th></tr>';
for (var r = 0; r < rows.length; r++) {
var i = rows[r];
var s = geoDateTagSuggestions[i];
var status = s.markForDelete ? '🗑️ درخواست حذف' : '➕ پیشنهاد جدید';
var coupletText = (s.coupletIndex != null && geoDateTagCoupletTexts[s.coupletIndex]) ? geoDateTagCoupletTexts[s.coupletIndex] : '— کل شعر —';
var personText = '';
if (!s.markForDelete) {
if (s.personId) {
personText = (allGeoPeople.filter(function (p) { return p.id === s.personId; })[0] || {}).name || ('نامبرده #' + s.personId);
} else if (s.suggestedPersonGraphJson) {
try {
var parsedGraph = JSON.parse(s.suggestedPersonGraphJson);
personText = 'نامبردۀ جدید: ' + (parsedGraph.person ? parsedGraph.person.name : '');
} catch (e) {
personText = 'نامبردۀ جدید';
}
}
}
html += '<tr><td>' + status + '</td><td>' + coupletText + '</td><td>' + personText + '</td>' +
'<td><a role="button" class="up-btn up-btn--sm up-btn--ghost" onclick="removeGeoDateTagSuggestion(' + i + ')">✕ حذف از فهرست</a></td></tr>';
}
html += '</table>';
container.innerHTML = html;
}
// the pending-suggestions table sits above the "add a new tag" card, so after adding/removing
// a row from it the change happens off-screen if the user is scrolled down to the form - jump
// them to the table so they can see what just happened
function scrollToPendingGeoDateTags() {
var container = document.getElementById('pending-geotag-suggestions');
if (container && container.innerHTML.trim() != '') {
container.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}
function scrollToPendingPersonTags() {
var container = document.getElementById('pending-persontag-suggestions');
if (container && container.innerHTML.trim() != '') {
container.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}
// shared by both sections - the two pending suggestions live in one combined array (see the
// comment above geoDateTagSuggestions), so removing by index and re-rendering both tables is
// simpler and just as cheap as tracking which table a given index belongs to
function removeGeoDateTagSuggestion(index) {
geoDateTagSuggestions.splice(index, 1);
renderPendingGeoDateTags();
renderPendingPersonTags();
}
function requestDeleteExistingGeoDateTag(existingTagId, el) {
geoDateTagSuggestions.push({
markForDelete: true,
existingTagId: existingTagId,
_uiKind: 'geo'
});
$(el).closest('tr').hide();
renderPendingGeoDateTags();
scrollToPendingGeoDateTags();
}
// person-tag counterpart of requestDeleteExistingGeoDateTag above
function requestDeleteExistingPersonTag(existingTagId, el) {
geoDateTagSuggestions.push({
markForDelete: true,
existingTagId: existingTagId,
_uiKind: 'person'
});
$(el).closest('tr').hide();
renderPendingPersonTags();
scrollToPendingPersonTags();
}
// Reads the "add a new geo/date tag" form (location + date only - see readPersonTagFromForm
// below for the separate شخصیت form). Returns a tag object if it has meaningful content, or
// null if the form is empty (with an optional error toast for the case where the user
// explicitly asked to add an empty entry vs. silently having nothing to pick up on save).
function readGeoDateTagFromForm(showErrorIfEmpty) {
var coupletIndex = $('#geotag-couplet-select').val();
var locationId = $('#geotag-location-select').val();
var newLocationName = $('#geotag-new-location-name').val();
var newLocationLat = $('#geotag-new-location-lat').val();
var newLocationLng = $('#geotag-new-location-lng').val();
var year = $('#geotag-year').val();
var month = $('#geotag-month').val();
var day = $('#geotag-day').val();
var hasLocation = (locationId && locationId != '') || (newLocationName && newLocationLat && newLocationLng);
var hasDate = year && year != '';
if (!hasLocation && !hasDate) {
if (showErrorIfEmpty) {
upToast('لطفاً حداقل یک مکان یا تاریخ وارد کنید.', 'error');
}
return null;
}
// coordinates are typed in by hand now (copied from Wikipedia/GeoHack), so a stray typo
// is more likely than when they came from a map click - catch obviously-wrong values here
if ((!locationId || locationId == '') && newLocationName) {
var latNum = parseFloat(newLocationLat);
var lngNum = parseFloat(newLocationLng);
if (isNaN(latNum) || isNaN(lngNum) || latNum < -90 || latNum > 90 || lngNum < -180 || lngNum > 180) {
upToast('عرض جغرافیایی باید بین ۹۰- تا ۹۰ و طول جغرافیایی باید بین ۱۸۰- تا ۱۸۰ باشد. مقدار وارد شده را با «مشاهده روی نقشه» بررسی کنید.', 'error');
return null;
}
}
return {
markForDelete: false,
coupletIndex: (coupletIndex && coupletIndex != '') ? parseInt(coupletIndex) : null,
locationId: (locationId && locationId != '') ? parseInt(locationId) : null,
suggestedLocationName: (!locationId || locationId == '') ? (newLocationName || null) : null,
suggestedLatitude: (!locationId || locationId == '') && newLocationLat ? parseFloat(newLocationLat) : null,
suggestedLongitude: (!locationId || locationId == '') && newLocationLng ? parseFloat(newLocationLng) : null,
lunarYear: year ? parseInt(year) : null,
lunarMonth: month ? parseInt(month) : null,
lunarDay: day ? parseInt(day) : null,
personId: null,
suggestedPersonGraphJson: null,
ignoreInCategory: $('#geotag-ignore-in-category').is(':checked'),
suggestionNote: $('#geotag-note').val() || null,
_uiKind: 'geo'
};
}
// شخصیت-tag counterpart of readGeoDateTagFromForm above - deliberately never sets
// locationId/suggestedLocationName/lunarYear/etc., so a person tag can never end up mixed with
// a geo/date tag in one row (see the comment above geoDateTagSuggestions for why that matters)
function readPersonTagFromForm(showErrorIfEmpty) {
var coupletIndex = $('#persontag-couplet-select').val();
var personId = $('#geotag-person-select').val();
var newPersonName = $('#geotag-new-person-name').val();
var hasPerson = (personId && personId != '') || (newPersonName && newPersonName.trim() != '');
if (!hasPerson) {
if (showErrorIfEmpty) {
upToast('لطفاً یک نامبردۀ موجود را انتخاب کنید یا نامبردۀ جدیدی تعریف کنید.', 'error');
}
return null;
}
var suggestedPersonGraphJson = null;
if ((!personId || personId == '') && newPersonName && newPersonName.trim() != '') {
suggestedPersonGraphJson = buildPersonGraphJson();
if (suggestedPersonGraphJson == null) {
return null; // buildPersonGraphJson already showed the relevant error toast
}
}
return {
markForDelete: false,
coupletIndex: (coupletIndex && coupletIndex != '') ? parseInt(coupletIndex) : null,
locationId: null,
suggestedLocationName: null,
suggestedLatitude: null,
suggestedLongitude: null,
lunarYear: null,
lunarMonth: null,
lunarDay: null,
personId: (personId && personId != '') ? parseInt(personId) : null,
suggestedPersonGraphJson: suggestedPersonGraphJson,
ignoreInCategory: false, // category-map aggregation is a geo/date-tag concept only
suggestionNote: $('#persontag-note').val() || null,
_uiKind: 'person'
};
}
function resetGeoDateTagForm() {
$('#geotag-couplet-select').val('');
$('#geotag-location-select').val('');
$('#geotag-location-search').val('');
$('#geotag-location-results').hide().empty();
$('#geotag-new-location-name').val('');
$('#geotag-new-location-lat').val('');
$('#geotag-new-location-lng').val('');
$('#geotag-year').val('');
$('#geotag-month').val('');
$('#geotag-day').val('');
$('#geotag-ignore-in-category').prop('checked', false);
$('#geotag-note').val('');
if (geoDateTagMarker != null && geoDateTagMap != null) {
geoDateTagMap.removeLayer(geoDateTagMarker);
geoDateTagMarker = null;
}
}
// شخصیت-tag counterpart of resetGeoDateTagForm above
function resetPersonTagForm() {
$('#persontag-couplet-select').val('');
$('#geotag-person-select').val('');
$('#geotag-person-search').val('');
$('#geotag-person-results').hide().empty();
$('#geotag-new-person-name').val('');
$('#geotag-new-person-description').val('');
$('#geotag-new-person-wikiurl').val('');
$('#geotag-new-person-familytreecaption').val('');
$('#geotag-new-person-importance').val('Normal');
$('#geotag-new-person-gender').val('Male');
$('#geotag-person-relatives-list').empty();
if (document.getElementById('new-person-widget').style.display !== 'none') {
toggleNewPersonWidget();
}
$('#persontag-note').val('');
}
async function addGeoDateTagSuggestion() {
var tag = readGeoDateTagFromForm(true);
if (tag == null) return;
var proceed = await _confirmNewLocationSuggestion(tag);
if (!proceed) return;
geoDateTagSuggestions.push(tag);
resetGeoDateTagForm();
renderPendingGeoDateTags();
scrollToPendingGeoDateTags();
upToast('به فهرست پیشنهادها افزوده شد. برای ثبت نهایی، دکمهٔ «ذخیره» را بزنید.');
}
// شخصیت-tag counterpart of addGeoDateTagSuggestion above - no location-duplicate confirmation
// needed here, that's a geo-tag-only concern
function addPersonTagSuggestion() {
var tag = readPersonTagFromForm(true);
if (tag == null) return;
geoDateTagSuggestions.push(tag);
resetPersonTagForm();
renderPendingPersonTags();
scrollToPendingPersonTags();
upToast('به فهرست پیشنهادها افزوده شد. برای ثبت نهایی، دکمهٔ «ذخیره» را بزنید.');
}
async function savePoemCorrections(id, verseCount) {
var verseOrderText = [];
var verseOrderMarkedForDelete = [];
var versePositions = [];
var verseOrderSummaries = [];
var verseLanguages = [];
for (var i = 0; i <= verseCount; i++) {
var oldElementName = '#original-' + i.toString();
var newElementName = '#id-' + i.toString();
var checkVIndex = '#marked-for-delete-' + i.toString();
var oldVersePosition = '#versepostion-original-' + i.toString();
var newVersePosition = '#versepostion-' + i.toString();
versePositions.push($(newVersePosition).val());
var oldVerseLanguage = '#language-old-' + i.toString();
var newVerseLanguage = '#language-' + i.toString();
verseLanguages.push($(newVerseLanguage).val());
var textChanged = $(newElementName).text() != $(oldElementName).text();
var checkedForDelete = i == 0 ? false : $(checkVIndex).is(":checked");
var positionChanged = i == 0 ? false : $(newVersePosition).val() != $(oldVersePosition).val();
var newLang = i == 0 ? '1' : $(newVerseLanguage).val();
var oldLang = i == 0 ? '1' : $(oldVerseLanguage).text();
var langChanged = newLang != oldLang;
if (textChanged || checkedForDelete || positionChanged || langChanged) {
verseOrderText.push(i.toString() + 'TextSeparator' + $(newElementName).text());
if ($(checkVIndex).is(":checked")) {
verseOrderMarkedForDelete.push(i);
}
}
var oldSummaryName = '#originalsummary-' + i.toString();
var newSummaryName = '#summary-' + i.toString();
if ($(oldSummaryName) != null && $(newSummaryName) != null && $(oldSummaryName).text() != $(newSummaryName).text()) {
verseOrderSummaries.push(i.toString() + 'TextSeparator' + $(newSummaryName).text());
}
}
var rhyme_letters = $('#rhyme-letters').val();
if (rhyme_letters == $('#rhyme-letters-old').text()) {
rhyme_letters = null;
}
var rhythm_new = $('#rhythm-new').val();
if (rhythm_new == $('#rhythm-old').text()) {
rhythm_new = null;
}
var rhythm_new2 = $('#rhythm-new2').val();
if (rhythm_new2 == $('#rhythm-old2').text()) {
rhythm_new2 = null;
}
var section_format = $('#poemformat').val();
if (section_format == $('#poemformat-old').text()) {
section_format = null;
}
// pick up anything still sitting in either "add a new tag" form (geo/date, and شخصیت) that
// the user didn't explicitly click "add to list" for - the explicit add button is only
// needed to stage more than one tag of the same kind before saving
var unstagedGeoDateTag = readGeoDateTagFromForm(false);
if (unstagedGeoDateTag != null) {
var proceedWithUnstaged = await _confirmNewLocationSuggestion(unstagedGeoDateTag);
if (!proceedWithUnstaged) {
// let them fix or clear the "add a new tag" form instead of silently dropping
// it or saving everything else without it
return;
}
geoDateTagSuggestions.push(unstagedGeoDateTag);
resetGeoDateTagForm();
renderPendingGeoDateTags();
scrollToPendingGeoDateTags();
}
var unstagedPersonTag = readPersonTagFromForm(false);
if (unstagedPersonTag != null) {
geoDateTagSuggestions.push(unstagedPersonTag);
resetPersonTagForm();
renderPendingPersonTags();
scrollToPendingPersonTags();
}
if (verseOrderText.length == 0 && verseOrderSummaries.length == 0 && geoDateTagSuggestions.length == 0) {
if (rhythm_new == null && rhythm_new2 == null && rhyme_letters == null && section_format == null) {
upToast('شما هیچ تغییری در اطلاعات نداده‌اید!', 'error');
return;
}
}
$('#savecorrections').hide();
$('#saving').text('در حال ذخیره ...');
var pcs = {};
pcs.poemid = id;
pcs.verseOrderText = verseOrderText;
pcs.verseOrderMarkedForDelete = verseOrderMarkedForDelete;
pcs.versePositions = versePositions;
pcs.rhythm = rhythm_new;
pcs.rhythm2 = rhythm_new2;
pcs.rhyme = rhyme_letters;
pcs.format = section_format;
pcs.verseOrderSummaries = verseOrderSummaries;
pcs.verseLanguages = verseLanguages;
pcs.note = $('#note').val();
pcs.hideMyName = $('#hide-my-name').is(":checked");
pcs.geoDateTags = geoDateTagSuggestions;
var url = '?handler=SendPoemCorrections';
$.ajax({
type: "POST",
url: url,
data: JSON.stringify(pcs),
contentType: "application/json; charset=utf-8",
dataType: "json",
success: function () {
location.reload(true);
},
error: function (e) {
upShowError(e.responseText || String(e));
$('#savecorrections').show();
$('#saving').text('');
}
});
}
function computePoemRhyme(poemId, elementId) {
$.ajax({
type: "GET",
url: '?Handler=ComputeRhyme',
data: {
id: poemId
},
error: function (e) {
upShowError(e.responseText || String(e));
},
success: function (rhyme) {
$(elementId).val(rhyme);
return false;
},
});
}
async function deletePoemCorrections(id) {
var ok = await upConfirm('آیا از حذف تغییرات پیشنهادی اطمینان دارید؟');
if (!ok) return;
$('#deletecorrections').hide();
$('#saving').text('در حال ذخیره ...');
var url = '?handler=DeletePoemCorrections';
$.ajax({
type: "POST",
url: url,
data: {
poemid: id,
},
error: function (e) {
upShowError(e.responseText || 'خطا رخ داد.');
},
success: function () {
location.reload(true);
},
});
}
function savemeta(id) {
$('#saving').text('در حال ذخیره ...');
var url = '?handler=SaveMeta';
$.ajax({
type: "POST",
url: url,
data: {
poemid: id,
noindex: $('#chk_noindex').is(":checked"),
redirectfromurl: $('#redirect_from_url').val(),
mixedmodeorder: $('#mixed_mode_order').val()
},
error: function (e) {
upShowError(e.responseText || String(e));
},
success: function () {
location.reload(true);
},
});
}
function internalEditorcompare(id, originalPrefix, editedPrefix, comparePrefix) {
var a = document.getElementById(originalPrefix + id.toString());
var b = document.getElementById(editedPrefix + id.toString());
var result = document.getElementById(comparePrefix + id.toString());
var changed = a.textContent !== b.textContent;
var origReveal = document.getElementById(originalPrefix + 'reveal-' + id.toString());
var compareReveal = document.getElementById(comparePrefix + 'reveal-' + id.toString());
if (origReveal) origReveal.style.display = changed ? '' : 'none';
if (compareReveal) compareReveal.style.display = changed ? '' : 'none';
if (!changed) {
result.textContent = a.textContent;
return;
}
var diff = JsDiff.diffChars(a.textContent, b.textContent);
var fragment = document.createDocumentFragment();
for (var i = 0; i < diff.length; i++) {
if (diff[i].added && diff[i + 1] && diff[i + 1].removed) {
var swap = diff[i];
diff[i] = diff[i + 1];
diff[i + 1] = swap;
}
var node;
if (diff[i].removed) {
node = document.createElement('del');
node.style.color = 'red';
node.appendChild(document.createTextNode(diff[i].value));
} else if (diff[i].added) {
node = document.createElement('span');
node.style.color = 'white';
node.style.backgroundColor = 'green';
node.appendChild(document.createTextNode(diff[i].value));
} else {
node = document.createTextNode(diff[i].value);
}
fragment.appendChild(node);
}
result.textContent = '';
result.appendChild(fragment);
}
function editorcompare(id) {
internalEditorcompare(id, 'original-', 'id-', 'compare-');
}
function summarycompare(id) {
internalEditorcompare(id, 'originalsummary-', 'summary-', 'comparesummary-');
}
async function breakPoem(poemId, vOrder, vText) {
var ok = await upConfirm(vText);
if (!ok) return;
$('#breakpoem-' + String(vOrder)).hide();
var url = '?handler=BreakPoem';
$.ajax({
type: "POST",
url: url,
data: {
poemId: poemId,
vOrder: vOrder
},
success: function (id) {
if (String(id) == '-1') {
upToast('کار شکستن شعر شروع شد.');
return;
}
location.href = '/?p=' + String(id);
},
error: function (e) {
upShowError(e.responseText || String(e));
$('#breakpoem-' + String(vOrder)).show();
}
});
}
function updateRelatedSections(meterId, rhymeletters) {
var url = '?handler=UpdateRelatedSections';
$.ajax({
type: "POST",
url: url,
data: {
meterId: meterId,
rhyme: rhymeletters,
},
success: function () {
upToast('انجام شد.');
},
error: function (e) {
upShowError(e.responseText || String(e));
}
});
}
function secondR() {
const secondtrs = document.querySelectorAll('#secondr1, #secondr2, #secondr3, #secondr4, #secondr5, #secondr6');
secondtrs.forEach(function (secondtr) {
secondtr.removeAttribute("style");
});
document.getElementById('secondr-btn').style.display = 'none';
}
async function updateCoupletIndices(id){
var ok = await upConfirm('آیا از بازسازی اندیس‌های ابیات اطمینان دارید؟');
if (!ok) return;
var url = '?handler=RefillCoupletIndices';
$.ajax({
type: "POST",
url: url,
data: {
id: id,
},
error: function (e) {
upShowError(e.responseText || String(e));
},
success: function () {
upToast('انجام شد.');
},
});
}
async function regenerateSections(id) {
var ok = await upConfirm('آیا از قطعه‌بندی مجدد اطمینان دارید؟');
if (!ok) return;
var ok2 = await upConfirm('قطعه‌بندی مجدد باعث حذف اطلاعات وزن‌ها می‌شود. آیا اطمینان دارید؟');
if (!ok2) return;
var url = '?handler=RegeneratePoemSections';
$.ajax({
type: "POST",
url: url,
data: {
id: id,
},
error: function (e) {
upShowError(e.responseText || String(e));
},
success: function () {
upToast('انجام شد.');
},
});
}
function newGeoDateTag(poemId) {
var url = '?handler=NewGeoDateTag';
var locationId = $('#location').val();
var year = $('#year').val();
var month = $('#month').val()
var day = $('#day').val()
$.ajax({
type: "POST",
url: url,
data: {
poemId: poemId,
locationId: locationId,
year: year,
month: month,
day: day,
verifiedDate: $("#verifiedDate").is(':checked'),
ignoreInCategory: $("#ignoreInCategory").is(':checked'),
},
success: function () {
location.reload();
},
error: function (e) {
upShowError(e.responseText || String(e));
}
});
}
async function deletTag(tagId) {
var ok = await upConfirm('آیا از حذف این برچسب اطمینان دارید؟');
if (!ok) return;
$.ajax({
type: "DELETE",
url: '?handler=GeoDateTag',
data: {
id: tagId,
},
success: function () {
upToast('انجام شد.');
location.reload();
},
error: function (e) {
upShowError(e.responseText || String(e));
}
});
}
</script>
@if (!string.IsNullOrEmpty(Model.FatalError))
{
<div class="up-alert up-alert--danger">@Model.FatalError</div>
}
else
{
<div class="up-alert up-alert--info">
<p><strong>نکات مهم در ویرایش متن</strong></p>
<ul>
<li><strong>ملاک، سندیت است</strong>: تصحیحات باید با منبع کاغذی گنجور برای آن شاعر مطابقت داشته باشد، نه با آنچه معروف‌تر، خوش‌آهنگ‌تر یا منطقی‌تر به نظر می‌رسد.</li>
<li><strong>یای کوتاه روی هٔ ناملفوظ</strong> (مثل «نامهٔ»): با میان‌بر Shift+N به‌صورت «هٔ» تایپ شود؛ نه «ه‌ی» و نه «ۀ». (استثنا: اگر یای روی «ه» واقعاً ساکن تلفظ می‌شود، به‌صورت یای کامل با سکون نوشته شود.)</li>
<li><strong>های مکسور</strong> (مثل «خانهٔ»): همین‌طور «خانهٔ» تایپ شود، نه «خانه‌ی».</li>
<li><strong>سرهم/جدا‌نویسی</strong>: اول به عرف رایج نگاه کنید؛ اگر معنای ترکیب با معنای دو کلمهٔ جدا فرق دارد، سرهم بنویسید. سرهم‌نویسی نادرست جستجوپذیری متن را خراب می‌کند.</li>
<li><strong>«که» چسبیده به «آن/این»</strong>: «کآن» و «کاین» بنویسید (نه «که‌آن»/«که‌این»، نه «کان»/«کین»). برای «و»: «وآن»/«وین» درست است، «واین» نه. مگر زمانی که به تناسب وزن واقعاً «که این»، «که آن» و ... خوانده می‌شوند.</li>
<li><strong>رسم‌الخط‌های کهن یا مختص یک نسخه</strong> (مثل «کی» به‌جای «که»، «سد» به‌جای «صد»): در صورت قطعیت کلمهٔ متداول، به شکل رایج امروزی برگردانده شود.</li>
<li><strong>علائم سجاوندی را کم به‌کار ببرید</strong>: به‌ویژه در شعر؛ نگذارید علامت‌گذاری یک خوانش را به خوانش‌های دیگرِ محتمل ترجیح دهد.</li>
<li><strong>فاصله‌گذاری علائم</strong>: قبل از علامت فاصله نگذارید، بعد از آن فاصله بگذارید («کدام دل؟» نه «کدام دل ؟»).</li>
<li><strong>جایگاه قافیه استثناست</strong>: اگر رعایت رسم‌الخط در قافیه به یکدستی قوافی شعر لطمه بزند، از آن صرف‌نظر کنید.</li>
<li><strong>کلمات فارسی را بی‌دلیل حرکت‌گذاری نکنید</strong>: تنها کلماتی را حرکت‌گذاری کنید که ممکن است اشتباه خوانده شوند.</li>
</ul>
</div>
<p><a role="button" target="_blank" href="@Model.PageInformation.FullUrl" class="up-btn up-btn--ghost">🔗 @Model.PageInformation.FullTitle</a></p>
if (Model.MyLastEdit != null)
{
<div class="up-card">
<h2 class="up-card__title">ویرایش‌های بررسی‌نشدهٔ من</h2>
<table>
<tr>
<td>متن اولیه</td>
<td>تغییرات من</td>
</tr>
@if (Model.MyLastEdit.Title != null)
{
<tr>
<td colspan="2">
تغییرات عنوان
</td>
</tr>
<tr>
<td>
<p class="red-color" id="title-lastedit-original">@Model.MyLastEdit.OriginalTitle</p>
</td>
<td>
<p class="green-color" id="title-lastedit-edited">@Model.MyLastEdit.Title</p>
</td>
</tr>
<tr>
<td colspan="2">
<p id="title-lastedit-compare">@Model.MyLastEdit.OriginalTitle</p>
<script>
$(document).ready(function () {
compareCorrectionBlocks('title-lastedit-original', 'title-lastedit-edited', 'title-lastedit-compare');
});
</script>
</td>
</tr>
}
@if (Model.MyLastEdit.Rhythm != null)
{
<tr>
<td colspan="2">
تغییرات وزن
</td>
</tr>
<tr>
<td>
<p class="red-color">@Model.MyLastEdit.OriginalRhythm</p>
</td>
<td>
<p class="green-color">@Model.MyLastEdit.Rhythm</p>
</td>
</tr>
}
@if (Model.MyLastEdit.Rhythm2 != null)
{
<tr>
<td colspan="2">
تغییرات وزن دوم
</td>
</tr>
<tr>
<td>
<p class="red-color">@Model.MyLastEdit.OriginalRhythm2</p>
</td>
<td>
<p class="green-color">@Model.MyLastEdit.Rhythm2</p>
</td>
</tr>
}
@if (Model.MyLastEdit.RhymeLetters != null)
{
<tr>
<td colspan="2">
تغییرات قافیه
</td>
</tr>
<tr>
<td>
<p class="red-color">@Model.RhymeLetters</p>
</td>
<td>
<p class="green-color">@Model.MyLastEdit.RhymeLetters</p>
</td>
</tr>
}
@if (Model.MyLastEdit.PoemFormat != null)
{
<tr>
<td colspan="2">
تغییرات قالب شعری
</td>
</tr>
<tr>
<td>
<p class="red-color"> @RMuseum.Services.Implementation.GanjoorPoemFormatConvertor.GetString(Model.MyLastEdit.OriginalPoemFormat)</p>
</td>
<td>
<p class="green-color"> @RMuseum.Services.Implementation.GanjoorPoemFormatConvertor.GetString(Model.MyLastEdit.PoemFormat)</p>
</td>
</tr>
}
@if (Model.MyLastEdit.PoemSummary != null)
{
<tr>
<td colspan="2">خلاصهٔ پیشنهادی من</td>
</tr>
<tr>
<td>
<p class="red-color" id="summary-lastedit-original">@Model.MyLastEdit.OriginalPoemSummary</p>
</td>
<td>
<p class="green-color" id="summary-lastedit-edited">@Model.MyLastEdit.PoemSummary</p>
</td>
</tr>
<tr>
<td colspan="2">
<p id="summary-lastedit-compare">@Model.MyLastEdit.OriginalPoemSummary</p>
<script>
$(document).ready(function () {
compareCorrectionBlocks('summary-lastedit-original', 'summary-lastedit-edited', 'summary-lastedit-compare');
});
</script>
</td>
</tr>
}
@if (Model.MyLastEdit.VerseOrderText != null && Model.MyLastEdit.VerseOrderText.Length > 0)
{
<tr>
<td colspan="2">
تغییرات متن
</td>
</tr>
@foreach (var verse in Model.MyLastEdit.VerseOrderText)
{
@if (verse.MarkForDelete)
{
<tr>
<td colspan="2">@verse.OriginalText</td>
</tr>
<tr>
<td>
<p>@verse.OriginalText</p>
</td>
<td>
<p class="red-color">باید حذف شود</p>
</td>
</tr>
}
else
{
@if (verse.VersePosition != null)
{
<tr>
<td colspan="2">@verse.OriginalText</td>
</tr>
<tr>
<td>
@if (verse.OriginalVersePosition != null)
{
<text>
نوع پیشین مصرع: <p class="red-color">@RMuseum.Models.Ganjoor.VersePositionHelper.GetVersePositionString((RMuseum.Models.Ganjoor.VersePosition)verse.OriginalVersePosition)</p>
</text>
}
</td>
<td>
نوع پیشنهادی مصرع: <p class="green-color">@RMuseum.Models.Ganjoor.VersePositionHelper.GetVersePositionString((RMuseum.Models.Ganjoor.VersePosition)verse.VersePosition)</p>
</td>
</tr>
}
@if (verse.Text != null)
{
<tr>
<td colspan="2">@verse.OriginalText</td>
</tr>
<tr>
<td>
<p class="red-color" id="verse@verse.Id-lastedit-original">@verse.OriginalText</p>
</td>
<td>
<p class="green-color" id="verse@verse.Id-lastedit-edited">@verse.Text</p>
</td>
</tr>
<tr>
<td colspan="2">
<p id="verse@verse.Id-lastedit-compare">@verse.OriginalText</p>
<script>
$(document).ready(function () {
compareCorrectionBlocks('verse@verse.Id-lastedit-original', 'verse@verse.Id-lastedit-edited', 'verse@verse.Id-lastedit-compare');
});
</script>
</td>
</tr>
}
@if (verse.LanguageId != null)
{
<tr>
<td colspan="2">@verse.OriginalText</td>
</tr>
<tr>
<td>
@if (verse.OriginalLanguageId == null)
{
<p class="red-color">زبان: فارسی</p>
}
else
{
<p class="red-color">زبان: @Model.Languages.Where(l => l.Id == verse.OriginalLanguageId).Single().Name</p>
}
</td>
<td>
<p class="green-color">@Model.Languages.Where(l => l.Id == verse.LanguageId).Single().Name</p>
</td>
</tr>
}
@if (verse.CoupletSummary != null)
{
<tr>
<td colspan="2">
<p>معنی @(verse.OriginalText + " ...")</p>
</td>
</tr>
<tr>
<td>
<p class="red-color" id="verse-summary@verse.Id-lastedit-original">@verse.OriginalCoupletSummary</p>
</td>
<td>
<p class="green-color" id="verse-summary@verse.Id-lastedit-edited">@verse.CoupletSummary</p>
</td>
</tr>
<tr>
<td colspan="2">
<p id="verse-summary@verse.Id-lastedit-compare">@verse.OriginalText</p>
<script>
$(document).ready(function () {
compareCorrectionBlocks('verse-summary@verse.Id-lastedit-original', 'verse-summary@verse.Id-lastedit-edited', 'verse-summary@verse.Id-lastedit-compare');
});
</script>
</td>
</tr>
}
}
}
}
@if (Model.MyLastEdit.GeoDateTags != null && Model.MyLastEdit.GeoDateTags.Length > 0)
{
<tr>
<td colspan="2">
برچسب‌های زمان و مکان پیشنهادی
</td>
</tr>
@foreach (var geoDateTag in Model.MyLastEdit.GeoDateTags)
{
<tr>
<td colspan="2">
<p class="up-hint">بیت: @(geoDateTag.CoupletIndex != null ? (Model.Couplets.Where(c => c.Item1 == geoDateTag.CoupletIndex).Select(c => c.Item2).FirstOrDefault() ?? "— کل شعر —") : "— کل شعر —")</p>
@if (geoDateTag.MarkForDelete)
{
<p class="red-color">درخواست حذف برچسب موجود (#@geoDateTag.ExistingTagId)</p>
}
else
{
<p class="green-color">
@if (geoDateTag.LocationId != null)
{
@(Model.Locations.Where(l => l.Id == geoDateTag.LocationId).Select(l => l.Name).FirstOrDefault())
}
else if (!string.IsNullOrEmpty(geoDateTag.SuggestedLocationName))
{
@($"{geoDateTag.SuggestedLocationName} (مکان جدید پیشنهادی)")
}
@if (geoDateTag.LunarYear != null)
{
@(" - سال " + geoDateTag.LunarYear)
}
@if (geoDateTag.LunarMonth != null)
{
switch (geoDateTag.LunarMonth)
{
case 1:
@(" محرم")
;
break;
case 2:
@(" صفر")
;
break;
case 3:
@(" ربیع‌الاول")
;
break;
case 4:
@(" ربیع‌الثانی")
;
break;
case 5:
@(" جمادی‌الاول")
;
break;
case 6:
@(" جمادی‌الثانی")
;
break;
case 7:
@(" رجب")
;
break;
case 8:
@(" شعبان")
;
break;
case 9:
@(" رمضان")
;
break;
case 10:
@(" شوال")
;
break;
case 11:
@(" ذیقعده")
;
break;
case 12:
@(" ذیحجه")
;
break;
}
}
@if (geoDateTag.LunarDay != null)
{
@(" روز " + geoDateTag.LunarDay)
}
@if (geoDateTag.IgnoreInCategory)
{
<text> (در نقشهٔ کلی مجموعه نادیده گرفته می‌شود)</text>
}
</p>
@if (!string.IsNullOrEmpty(geoDateTag.SuggestionNote))
{
<p class="up-hint">یادداشت: @geoDateTag.SuggestionNote</p>
}
}
</td>
</tr>
}
}
@if (!string.IsNullOrEmpty(Model.MyLastEdit.Note))
{
<tr>
<td>یادداشت:</td>
<td>
@Model.MyLastEdit.Note
</td>
</tr>
}
<tr>
<td colspan="2" class="td-button-container">
<a role="button" id="deletecorrections" class="up-btn up-btn--danger" onclick="deletePoemCorrections(@Model.PageInformation.Poem.Id)">🗑 انصراف و حذف این ویرایش</a>
</td>
</tr>
</table>
</div>
}
<p class="up-btn-group-label">پیمایش</p>
<div class="up-btn-group">
<a href="/User/PoemCorrectionsHistory?id=@Model.PageInformation.Id" role="button" class="up-btn up-btn--ghost" target="_blank">🕘 سوابق ویرایش</a>
@if (Model.PageInformation.Poem.Previous != null)
{
@if (Model.ShowAdminOps)
{
<a href="/User/Editor?admin=1&id=@Model.PageInformation.Poem.Previous.Id" role="button" class="up-btn up-btn--ghost">شعر قبل ▶</a>
}
else
{
<a href="/User/Editor?id=@Model.PageInformation.Poem.Previous.Id" role="button" class="up-btn up-btn--ghost">شعر قبل ▶</a>
}
}
@if (Model.PageInformation.Poem.Next != null)
{
@if (Model.ShowAdminOps)
{
<a href="/User/Editor?admin=1&id=@Model.PageInformation.Poem.Next.Id" role="button" class="up-btn up-btn--ghost">◀ شعر بعد</a>
}
else
{
<a href="/User/Editor?id=@Model.PageInformation.Poem.Next.Id" role="button" class="up-btn up-btn--ghost">◀ شعر بعد</a>
}
}
</div>
<p class="up-btn-group-label">لغزش سریع</p>
<div class="up-btn-group">
@if (Model.CanAssignRhythms)
{
<a role="button" href="#rhythms-section" class="up-btn up-btn--sm up-btn--ghost">🎵 بخش وزن، قافیه و قالب شعری</a>
}
else
{
<a role="button" href="#poem-sections" class="up-btn up-btn--sm up-btn--ghost">🧩 بخش قطعات</a>
}
<a role="button" href="#summaries" class="up-btn up-btn--sm up-btn--ghost">💬 بخش معنی</a>
<a role="button" href="#geo-date-section" class="up-btn up-btn--sm up-btn--ghost">🗺️ بخش زمان و مکان</a>
<a role="button" href="#person-tag-section" class="up-btn up-btn--sm up-btn--ghost">🧑‍🎓 بخش نامبردگان</a>
<a role="button" href="#footer" class="up-btn up-btn--sm up-btn--ghost">⬇ پایین صفحه</a>
</div>
@if (Model.CanEdit && !Model.ShowAdminOps)
{
<div class="up-btn-group">
<a role="button" href="/User/Editor?id=@Model.PageInformation.Id&admin=1" class="up-btn up-btn--ghost">⚙️ امکانات مدیریتی</a>
</div>
}
@if (Model.TextSourceImage != null)
{
<div class="up-card">
<p><strong>منبع کاغذی:</strong></p>
<p class="up-hint">(متن پیشنهادی نمی‌بایست از نسخهٔ متفاوتی باشد)</p>
<div class="related-images">
<div class="related-image-container">
<a target="_blank" href="@Html.Raw(Model.TextSourceImage.TargetPageUrl)">
<img src="@Html.Raw(Model.TextSourceImage.ThumbnailImageUrl)" alt="@Model.TextSourceImage.AltText" loading="lazy" class="related-image-main">
</a>
</div>
</div>
</div>
}
<div class="up-card">
<h2 class="up-card__title">ویرایش متن</h2>
<div class="up-field">
<label for="id-0"><span class="up-field-icon" aria-hidden="true">🏷️</span> عنوان ویراستهٔ من</label>
@if (Model.MyLastEdit != null && Model.MyLastEdit.Title != null)
{
<div id="id-0" name="id-0" contenteditable="true" class="up-editable">@Model.MyLastEdit.Title</div>
<script>
$(document).ready(function () {
editorcompare(0);
});
document.getElementById("id-0").addEventListener("input", function () {
editorcompare(0)
}, false);
</script>
}
else
{
<div type="text" id="id-0" name="id-0" contenteditable="true" class="up-editable">@Model.PageInformation.Poem.Title</div>
<script>
document.getElementById("id-0").addEventListener("input", function () {
editorcompare(0)
}, false);
</script>
}
</div>
<div id="original-reveal-0" class="up-reveal" style="display:none">
<div class="up-field">
<label>عنوان فعلی</label>
<p id="original-0" class="up-field-static">@Model.PageInformation.Poem.Title</p>
</div>
</div>
<div id="compare-reveal-0" class="up-reveal" style="display:none">
<div class="up-field">
<label>تغییرات</label>
<p id="compare-0" class="up-field-static">@Model.PageInformation.Poem.Title</p>
</div>
</div>
</div>
<div class="up-card">
<h2 class="up-card__title">ویرایش مصرع‌ها</h2>
@foreach (var verse in Model.PageInformation.Poem.Verses)
{
var verseCoupletNumber = Model.GetVerseCoupletNumber(verse);
<div class="up-verse-row" id="tr-@verse.VOrder">
<p>
<span class="up-verse-number">@verseCoupletNumber.ToPersianNumbers()</span>
@{ var verseProgressPercent = Model.GetCoupletProgressPercent(verseCoupletNumber); }
<span class="up-verse-progress" title="@verseCoupletNumber.ToPersianNumbers() از @Model.TotalCoupletsCount.ToPersianNumbers()"><span class="up-verse-progress-fill" style="width:@(verseProgressPercent)%;--up-progress-color:@(Model.GetCoupletProgressColor(verseProgressPercent))"></span></span>
<span class="up-hint">ترتیب مصرع یا خط از ابتدا: @verse.VOrder.ToPersianNumbers()</span>
</p>
<div class="up-field">
<label for="id-@($"{verse.VOrder}")"><span class="up-field-icon" aria-hidden="true">📝</span> ویراستهٔ من</label>
@if (Model.MyLastEdit != null && Model.MyLastEdit.VerseOrderText != null && Model.MyLastEdit.VerseOrderText.Where(v => v.VORder == verse.VOrder && !string.IsNullOrEmpty(v.Text)).Any())
{
<div id="id-@($"{verse.VOrder}")" name="name-@($"{verse.VOrder}")" contenteditable="true" class="up-editable">@Model.MyLastEdit.VerseOrderText.Where(v => v.VORder == verse.VOrder).First().Text</div>
<script>
document.getElementById("id-@($"{verse.VOrder}")").addEventListener("input", function () {
editorcompare(@verse.VOrder)
}, false);
$(document).ready(function () {
editorcompare(@verse.VOrder);
});
</script>
}
else
{
<div id="id-@($"{verse.VOrder}")" name="name-@($"{verse.VOrder}")" contenteditable="true" class="up-editable">@verse.Text</div>
<script>
document.getElementById("id-@($"{verse.VOrder}")").addEventListener("input", function () {
editorcompare(@verse.VOrder)
}, false);
</script>
}
</div>
<div id="original-reveal-@($"{verse.VOrder}")" class="up-reveal" style="display:none">
<div class="up-field">
<label>متن فعلی</label>
<p id="original-@($"{verse.VOrder}")" class="up-field-static">@verse.Text</p>
</div>
</div>
<div id="compare-reveal-@($"{verse.VOrder}")" class="up-reveal" style="display:none">
<div class="up-field">
<label>تغییرات</label>
<p id="compare-@($"{verse.VOrder}")" class="up-field-static">@verse.Text</p>
</div>
</div>
<div class="up-field-row">
<div class="up-field">
<label for="versepostion-@($"{verse.VOrder}")"><span class="up-field-icon" aria-hidden="true">🔀</span> نوع</label>
<select id="versepostion-@($"{verse.VOrder}")" name="versepostion-@($"{verse.VOrder}")">
@if (Model.MyLastEdit != null && Model.MyLastEdit.VerseOrderText != null && Model.MyLastEdit.VerseOrderText.Where(v => v.VORder == verse.VOrder && v.VersePosition != null).Any())
{
@foreach (var verseposition in RMuseum.Models.Ganjoor.VersePositionHelper.VersePositions)
if (verseposition.VersePosition == Model.MyLastEdit.VerseOrderText.Where(v => v.VORder == verse.VOrder).First().VersePosition)
{
<option value="@verseposition.VersePosition" selected>@verseposition.Text</option>
}
else
{
<option value="@verseposition.VersePosition">@verseposition.Text</option>
}
}
else
{
@foreach (var verseposition in RMuseum.Models.Ganjoor.VersePositionHelper.VersePositions)
if (verseposition.VersePosition == verse.VersePosition)
{
<option value="@verseposition.VersePosition" selected>@verseposition.Text</option>
}
else
{
<option value="@verseposition.VersePosition">@verseposition.Text</option>
}
}
</select>
@if (Model.MyLastEdit != null && Model.MyLastEdit.VerseOrderText != null && Model.MyLastEdit.VerseOrderText.Where(v => v.VORder == verse.VOrder).Any())
{
<input type="hidden" id="versepostion-original-@($"{verse.VOrder}")" name="versepostion-original-@($"{verse.VOrder}")" value="@Model.MyLastEdit.VerseOrderText.Where(v => v.VORder == verse.VOrder).First().VersePosition" />
}
else
{
<input type="hidden" id="versepostion-original-@($"{verse.VOrder}")" name="versepostion-original-@($"{verse.VOrder}")" value="@verse.VersePosition" />
}
</div>
<div class="up-field">
<label for="language-@verse.VOrder"><span class="up-field-icon" aria-hidden="true">🌐</span> زبان</label>
<select id="language-@verse.VOrder">
@foreach (var lang in Model.Languages)
{
@if (Model.MyLastEdit != null && Model.MyLastEdit.VerseOrderText != null && Model.MyLastEdit.VerseOrderText.Where(v => v.VORder == verse.VOrder && v.LanguageId == lang.Id).Any())
{
<option value="@lang.Id" selected>@lang.Name</option>
}
else if (verse.LanguageId == null && lang.Id == 1)
{
<option value="1" selected>فارسی</option>
}
else if (verse.LanguageId == lang.Id)
{
<option value="@lang.Id" selected>@lang.Name</option>
}
else
{
<option value="@lang.Id">@lang.Name</option>
}
}
</select>
@if (verse.LanguageId == null)
{
<span id="language-old-@verse.VOrder" class="hidden-recitation">1</span>
}
else
{
<span id="language-old-@verse.VOrder" class="hidden-recitation">@Model.Languages.Where(l => l.Id == verse.LanguageId).Single().Id</span>
}
</div>
</div>
<div class="up-check-row">
@if (Model.MyLastEdit != null && Model.MyLastEdit.VerseOrderText != null && Model.MyLastEdit.VerseOrderText.Where(v => v.VORder == verse.VOrder && v.MarkForDelete).Any())
{
<input type="checkbox" name="marked-for-delete-@verse.VOrder" id="marked-for-delete-@verse.VOrder" onchange="toggleMarkedForDelete(@verse.VOrder)" checked />
}
else
{
<input type="checkbox" name="marked-for-delete-@verse.VOrder" id="marked-for-delete-@verse.VOrder" onchange="toggleMarkedForDelete(@verse.VOrder)" />
}
<label for="marked-for-delete-@verse.VOrder">باید حذف شود</label>
</div>
<script>
$(document).ready(function () {
toggleMarkedForDelete(@verse.VOrder);
});
</script>
<div class="up-btn-group">
<a role="button" href="#savecorrections" class="up-btn up-btn--sm up-btn--save-jump up-btn--block">⬇ رفتن به ذخیره</a>
@if (Model.ShowAdminOps && verse.VOrder > 1 && verse.VersePosition != RMuseum.Models.Ganjoor.VersePosition.Left)
{
<a role="button" id="breakpoem-@verse.VOrder" class="up-btn up-btn--sm up-btn--danger" onclick="breakPoem(@Model.PageInformation.Poem.Id, @verse.VOrder, '@verse.Text')">✂️ شکستن از اینجا</a>
}
</div>
</div>
}
</div>
@if (!Model.CanAssignRhythms)
{
<div class="up-alert up-alert--warning">این مورد شامل بیش از یک قطعه شعر یا دارای پاراگراف نثر است و امکان انتساب وزن و قالب شعری به آن و یا تعیین حروف قافیهٔ آن وجود ندارد.</div>
}
else
{
<div class="up-card" id="rhythms-section">
<h2 class="up-card__title">وزن</h2>
<div class="up-field">
<label>وزن فعلی</label>
<p id="rhythm-old" class="up-field-static">@(Model.GanjoorMetre1 == null ? "" : Model.GanjoorMetre1.Rhythm)</p>
</div>
<div class="up-alert up-alert--warning">
<p class="up-mt-0">توجه داشته باشید که کارکرد برچسب‌گذاری اشعار در گنجور یافتن اشعار هم‌وزن است. از این جهت از پیشنهاد صورت‌های سلیقه‌ای و متفاوت یک وزن خودداری کنید تا این قابلیت مختل نشود. <a href="https://ganjoor.net/faq?id=14" target="_blank">این نکته</a> را هم مد نظر داشته باشید.</p>
</div>
<div class="up-field">
<label for="select-search"><span class="up-field-icon" aria-hidden="true">🎵</span> جستجوی وزن</label>
<input type="text" id="select-search" value="" oninput="doSearchInRhythmsCombo('select-search', 'rhythm-new')" list="inputRhythms" autocomplete="off" />
<datalist id="inputRhythms">
@if (Model.RhythmsByVerseCount != null)
@foreach (var rhythm in Model.RhythmsByVerseCount)
{
<option value="@rhythm.Rhythm">@rhythm.Rhythm</option>
}
</datalist>
</div>
<div class="up-field">
<label for="rhythm-new"><span class="up-field-icon" aria-hidden="true">🎵</span> وزن ویراستهٔ من</label>
<select id="rhythm-new" name="rhythm-new">
@foreach (var rhythm in Model.RhythmsAlphabetically)
{
if (Model.MyLastEdit != null && Model.MyLastEdit.Rhythm == rhythm.Rhythm)
{
<option value="@rhythm.Rhythm" selected>@rhythm.Rhythm</option>
}
else
{
if (Model.GanjoorMetre1 != null && Model.GanjoorMetre1.Rhythm == rhythm.Rhythm)
{
<option value="@rhythm.Rhythm" selected>@rhythm.Rhythm</option>
}
else
{
<option value="@rhythm.Rhythm">@rhythm.Rhythm</option>
}
}
}
</select>
<div class="up-btn-group up-mt-0">
<a role="button" onclick="resetRhythm('rhythm-new')" class="up-btn up-btn--sm up-btn--ghost">🧹 پاک کردن وزن</a>
<script>
function newRhythm() {
var rhythm = prompt("پیشنهاد وزن جدید", "");
if (rhythm != null) {
var select = document.getElementById('rhythm-new');
var options = select.options;
for (let i = 0; i < options.length; i++) {
if (options[i].value == rhythm) {
upToast('وزن پیشنهادی تکراری است.', 'error');
return;
}
}
var opt = document.createElement('option');
opt.value = rhythm;
opt.innerHTML = rhythm;
select.appendChild(opt);
var opt2 = document.createElement('option');
opt2.value = rhythm;
opt2.innerHTML = rhythm;
document.getElementById('rhythm-new2').appendChild(opt2);
upToast('وزن پیشنهادی به انتهای لیست وزن‌ها اضافه شده. لطفاً از لیست آن را انتخاب کنید.');
}
}
</script>
<a role="button" onclick="newRhythm()" class="up-btn up-btn--sm up-btn--ghost">➕ پیشنهاد وزن جدید</a>
</div>
</div>
<div class="up-field">
<label>حروف قافیه و ردیفِ فعلی</label>
<p id="rhyme-letters-old" class="up-field-static">@Model.RhymeLetters</p>
</div>
<div class="up-field">
<label for="rhyme-letters"><span class="up-field-icon" aria-hidden="true">🔤</span> حروف قافیه و ردیفِ ویراستهٔ من (بدون فاصله و حرکات)</label>
<div class="up-btn-group up-mt-0">
@if (Model.MyLastEdit != null && !string.IsNullOrEmpty(Model.MyLastEdit.RhymeLetters))
{
<input type="text" id="rhyme-letters" name="rhyme-letters" value="@Model.MyLastEdit.RhymeLetters" style="flex:1;" />
}
else
{
<input type="text" id="rhyme-letters" name="rhyme-letters" value="@Model.RhymeLetters" style="flex:1;" />
}
<a role="button" onclick="computePoemRhyme(@Model.PageInformation.Id, '#rhyme-letters')" class="up-btn up-btn--ghost">🧮 محاسبه</a>
</div>
<p class="up-hint">
این فیلد قافیه و ردیف را با هم و بدون فاصله می‌خواهد، نه فقط قافیه. اگر بعد از حروف قافیه کلمه یا کلماتی عیناً در همهٔ ابیات تکرار شده باشند (ردیف)، آن‌ها هم باید در انتها بیایند.
برای نمونه در بیت «... اگر تو <b>یله کنی</b>» / «... اگر تو <b>گله کنی</b>» چون «کنی» عیناً در هر دو مصرع تکرار شده و ردیف شعر است، حروف قافیه و ردیف باید «<b>لهکنی</b>» نوشته شود؛ نه «له» (با نادیده گرفتن ردیف) و نه «ه کنی» (با فاصلهٔ اشتباه).
همچنین از حرکات (فتحه، ضمه، کسره) یا هر نشانهٔ اضافهٔ دیگر در این فیلد استفاده نشود.
</p>
</div>
<div id="secondr-btn">
<a role="button" onclick="secondR()" class="up-btn up-btn--sm up-btn--ghost">🎶 این شعر دو وزنی است</a>
</div>
<div id="secondr1" style="display:none">
<h3 class="up-card__title">وزن دوم (شعرهای چندوزنی)</h3>
</div>
<div id="secondr2" class="up-alert up-alert--warning" style="display:none">
<p class="up-mt-0">تعدادی کمی از شعرها دو وزنی هستند.</p>
</div>
<div id="secondr3" class="up-field" style="display:none">
<label>وزن دوم فعلی</label>
<p id="rhythm-old2" class="up-field-static">@(Model.GanjoorMetre2 == null ? "" : Model.GanjoorMetre2.Rhythm)</p>
</div>
<div id="secondr4" class="up-field" style="display:none">
<label for="select-search2"><span class="up-field-icon" aria-hidden="true">🎵</span> جستجوی وزن (دوم)</label>
<input type="text" id="select-search2" value="" oninput="doSearchInRhythmsCombo('select-search2', 'rhythm-new2')" list="inputRhythms2" autocomplete="off" />
<datalist id="inputRhythms2">
@if (Model.RhythmsByVerseCount != null)
@foreach (var rhythm in Model.RhythmsByVerseCount)
{
<option value="@rhythm.Rhythm">@rhythm.Rhythm</option>
}
</datalist>
</div>
<div id="secondr5" class="up-field" style="display:none">
<label for="rhythm-new2"><span class="up-field-icon" aria-hidden="true">🎵</span> وزن (دوم) انتخاب شده</label>
<select id="rhythm-new2" name="rhythm-new2">
@foreach (var rhythm in Model.RhythmsAlphabetically)
{
if (Model.MyLastEdit != null && Model.MyLastEdit.Rhythm2 == rhythm.Rhythm)
{
<option value="@rhythm.Rhythm" selected>@rhythm.Rhythm</option>
}
else
{
if (Model.GanjoorMetre2 != null && Model.GanjoorMetre2.Rhythm == rhythm.Rhythm)
{
<option value="@rhythm.Rhythm" selected>@rhythm.Rhythm</option>
}
else
{
<option value="@rhythm.Rhythm">@rhythm.Rhythm</option>
}
}
}
</select>
<a role="button" onclick="resetRhythm('rhythm-new2')" class="up-btn up-btn--sm up-btn--ghost">🧹 پاک کردن وزن</a>
</div>
<div id="secondr6" style="display:none"></div>
<div class="up-field">
<label>قالب شعری فعلی</label>
<p class="up-field-static">
@RMuseum.Services.Implementation.GanjoorPoemFormatConvertor.GetString(Model.PoemFormat)
<span id="poemformat-old" class="hidden-recitation">@Model.PoemFormat</span>
</p>
</div>
<div class="up-field">
<label for="poemformat"><span class="up-field-icon" aria-hidden="true">📐</span> قالب شعری</label>
<select id="poemformat">
@foreach (var poemFormat in System.Enum.GetValues(typeof(RMuseum.Models.Ganjoor.GanjoorPoemFormat)))
{
@if (Model.MyLastEdit != null && (Model.MyLastEdit.PoemFormat == (RMuseum.Models.Ganjoor.GanjoorPoemFormat)poemFormat))
{
<option value="@poemFormat" selected>@RMuseum.Services.Implementation.GanjoorPoemFormatConvertor.GetString((RMuseum.Models.Ganjoor.GanjoorPoemFormat)poemFormat)</option>
}
else if (Model.PoemFormat == (RMuseum.Models.Ganjoor.GanjoorPoemFormat)poemFormat)
{
<option value="@poemFormat" selected>@RMuseum.Services.Implementation.GanjoorPoemFormatConvertor.GetString((RMuseum.Models.Ganjoor.GanjoorPoemFormat)poemFormat)</option>
}
else
{
<option value="@poemFormat">@RMuseum.Services.Implementation.GanjoorPoemFormatConvertor.GetString((RMuseum.Models.Ganjoor.GanjoorPoemFormat)poemFormat)</option>
}
}
</select>
</div>
</div>
}
<div class="up-card">
<div class="up-field">
<label for="note"><span class="up-field-icon" aria-hidden="true">🗒️</span> توضیح این ویرایش</label>
@if (Model.MyLastEdit != null && !string.IsNullOrEmpty(Model.MyLastEdit.Note))
{
<input type="text" id="note" name="note" value="@Model.MyLastEdit.Note" />
}
else
{
<input type="text" id="note" name="note" value="" />
}
</div>
</div>
<div class="up-card" id="summaries">
<h2 class="up-card__title">خلاصه و معنی</h2>
<div class="up-alert up-alert--warning">
<p>توجه فرمایید که هدف از این بخش، تفسیر متن اشعار <span style="color:red">نیست</span>. انتظار می‌رود در این بخش برای متون و اشعاری مثل داستان‌ها خلاصه یا لب مطلب را بنویسید (برای بعضی از انواع اشعار مثل غزل‌ها ممکن است خلاصهٔ کلی معنی نداشته باشد). برای خطوط یا ابیات نیز برگردان متن به نثر قابل فهم امروزی نوشته شود (لازم نیست برای همهٔ خطوط برگردان نثر نوشته شود). برای تفسیر یا درج عقاید شخصی خود راجع به اشعار از حاشیه‌ها استفاده کنید.</p>
<p>متن باید خلاصه، به فارسی نوشتاری رسمی (غیرمحاوره‌ای)، خالی از لغات نامأنوس (زبان همه‌کس فهم) و بدون غلط املایی و تایپی باشد.</p>
<p>اگر متن موجود به اندازهٔ کافی خوب باشد ممکن است متن خیلی متفاوت جایگزین تأیید نشود.</p>
<p>نام و مشخصات نویسندهٔ متن در صفحهٔ شعر نمایش داده نمی‌شود اما در تاریخچهٔ تغییرات نگهداری می‌شود.</p>
<p class="up-mt-0">در صورتی که متن تولید شده توسط <span style="color:red">هوش مصنوعی</span> را ویرایش می‌کنید عبارت نشان‌دهندهٔ تولید هوش مصنوعی بودن آن را <span style="color:red">حذف کنید</span>.</p>
</div>
<h3 class="up-card__title">خلاصه یا مفهوم کلی شعر یا متن</h3>
<p class="up-hint">اگر متن یا شعر یک مفهوم و خلاصهٔ کلی ندارد (مثل خیلی از غزلیات حافظ) برای آن متن کلی وارد نکنید.</p>
<div class="up-field">
<label for="summary-0"><span class="up-field-icon" aria-hidden="true">💬</span> خلاصهٔ ویراستهٔ من (به زبان غیرمحاوره‌ای و همه‌کس فهم)</label>
@if (Model.MyLastEdit != null && Model.MyLastEdit.PoemSummary != null)
{
<div id="summary-0" name="summary-0" contenteditable="true" class="up-editable">@Model.MyLastEdit.PoemSummary</div>
<script>
document.getElementById("summary-0").addEventListener("input", function () {
summarycompare(0)
}, false);
$(document).ready(function () {
summarycompare(0);
});
</script>
}
else
{
<div id="summary-0" name="summary-0" contenteditable="true" class="up-editable">@Model.PageInformation.Poem.PoemSummary</div>
<script>
document.getElementById("summary-0").addEventListener("input", function () {
summarycompare(0)
}, false);
</script>
}
@if (string.IsNullOrEmpty(Model.PageInformation.Poem.PoemSummary))
{
<p class="up-hint">هنوز متنی برای خلاصهٔ کلی نوشته نشده است.</p>
}
</div>
<div id="originalsummary-reveal-0" class="up-reveal" style="display:none">
<div class="up-field">
<label>خلاصهٔ فعلی</label>
<p id="originalsummary-0" class="up-field-static">@Model.PageInformation.Poem.PoemSummary</p>
</div>
</div>
<div id="comparesummary-reveal-0" class="up-reveal" style="display:none">
<div class="up-field">
<label>تغییرات</label>
<p id="comparesummary-0" class="up-field-static">@Model.PageInformation.Poem.PoemSummary</p>
</div>
</div>
<a role="button" href="#savecorrections" class="up-btn up-btn--sm up-btn--save-jump up-btn--block">⬇ رفتن به ذخیره</a>
</div>
<div class="up-card">
<h2 class="up-card__title">برگردان خطوط به زبان ساده</h2>
@for (int verseIndex = 0; verseIndex < Model.PageInformation.Poem.Verses.Length; verseIndex++)
{
@if (
Model.PageInformation.Poem.Verses[verseIndex].VersePosition == RMuseum.Models.Ganjoor.VersePosition.Right
|| Model.PageInformation.Poem.Verses[verseIndex].VersePosition == RMuseum.Models.Ganjoor.VersePosition.CenteredVerse1
|| Model.PageInformation.Poem.Verses[verseIndex].VersePosition == RMuseum.Models.Ganjoor.VersePosition.Paragraph
)
{
var summaryCoupletNumber = Model.GetVerseCoupletNumber(Model.PageInformation.Poem.Verses[verseIndex]);
<div class="up-verse-row">
<p>
<span class="up-verse-number">@summaryCoupletNumber.ToPersianNumbers()</span>
@{ var summaryProgressPercent = Model.GetCoupletProgressPercent(summaryCoupletNumber); }
<span class="up-verse-progress" title="@summaryCoupletNumber.ToPersianNumbers() از @Model.TotalCoupletsCount.ToPersianNumbers()"><span class="up-verse-progress-fill" style="width:@(summaryProgressPercent)%;--up-progress-color:@(Model.GetCoupletProgressColor(summaryProgressPercent))"></span></span>
</p>
<div class="up-field">
<label>متن</label>
@if (Model.PageInformation.Poem.Verses[verseIndex].VersePosition == RMuseum.Models.Ganjoor.VersePosition.Paragraph)
{
<p class="up-field-static">@Model.PageInformation.Poem.Verses[verseIndex].Text</p>
}
else
{
<div class="up-field-static">
@for (int nIndex = verseIndex; nIndex < Model.PageInformation.Poem.Verses.Length &&
(nIndex == verseIndex
||
(
Model.PageInformation.Poem.Verses[nIndex].VersePosition != RMuseum.Models.Ganjoor.VersePosition.Right
&& Model.PageInformation.Poem.Verses[nIndex].VersePosition != RMuseum.Models.Ganjoor.VersePosition.CenteredVerse1
&& Model.PageInformation.Poem.Verses[nIndex].VersePosition != RMuseum.Models.Ganjoor.VersePosition.Paragraph
))
; nIndex++)
{
<div>@Model.PageInformation.Poem.Verses[nIndex].Text</div>
}
</div>
}
</div>
<div class="up-alert up-alert--warning">
<p class="up-mt-0"><span style="color:red">تذکّر مهم:</span> از این کادر برای نوشتن چیزی جز معنی استفاده <span style="color:red">نکنید</span>.</p>
<p class="up-mt-0">در صورتی که متن تولید شده توسط هوش مصنوعی را ویرایش می‌کنید عبارت نشان‌دهندهٔ تولید <span style="color:red">هوش مصنوعی</span> بودن آن را <span style="color:red">حذف کنید</span>.</p>
</div>
<div class="up-field">
<label for="summary-@($"{Model.PageInformation.Poem.Verses[verseIndex].VOrder}")"><span class="up-field-icon" aria-hidden="true">💬</span> معنای ویراستهٔ من (به زبان غیرمحاوره‌ای و همه‌کس فهم)</label>
@if (Model.MyLastEdit != null && Model.MyLastEdit.VerseOrderText != null && Model.MyLastEdit.VerseOrderText.Where(v => v.VORder == Model.PageInformation.Poem.Verses[verseIndex].VOrder && !string.IsNullOrEmpty(v.CoupletSummary)).Any())
{
<div id="summary-@($"{Model.PageInformation.Poem.Verses[verseIndex].VOrder}")" name="summary-@($"{Model.PageInformation.Poem.Verses[verseIndex].VOrder}")" contenteditable="true" class="up-editable">@Model.MyLastEdit.VerseOrderText.Where(v => v.VORder == Model.PageInformation.Poem.Verses[verseIndex].VOrder).First().CoupletSummary</div>
<script>
document.getElementById("summary-@($"{Model.PageInformation.Poem.Verses[verseIndex].VOrder}")").addEventListener("input", function () {
summarycompare(@Model.PageInformation.Poem.Verses[verseIndex].VOrder)
}, false);
$(document).ready(function () {
summarycompare(@Model.PageInformation.Poem.Verses[verseIndex].VOrder);
});
</script>
}
else
{
<div id="summary-@($"{Model.PageInformation.Poem.Verses[verseIndex].VOrder}")" name="summary-@($"{Model.PageInformation.Poem.Verses[verseIndex].VOrder}")" contenteditable="true" class="up-editable">@Model.PageInformation.Poem.Verses[verseIndex].CoupletSummary</div>
<script>
document.getElementById("summary-@($"{Model.PageInformation.Poem.Verses[verseIndex].VOrder}")").addEventListener("input", function () {
summarycompare(@Model.PageInformation.Poem.Verses[verseIndex].VOrder)
}, false);
</script>
}
@if (string.IsNullOrEmpty(Model.PageInformation.Poem.Verses[verseIndex].CoupletSummary))
{
<p class="up-hint">هنوز متنی برای معنی نوشته نشده است.</p>
}
</div>
<div id="originalsummary-reveal-@($"{Model.PageInformation.Poem.Verses[verseIndex].VOrder}")" class="up-reveal" style="display:none">
<div class="up-field">
<label>معنای فعلی</label>
<p id="originalsummary-@($"{Model.PageInformation.Poem.Verses[verseIndex].VOrder}")" class="up-field-static">@Model.PageInformation.Poem.Verses[verseIndex].CoupletSummary</p>
</div>
</div>
<div id="comparesummary-reveal-@($"{Model.PageInformation.Poem.Verses[verseIndex].VOrder}")" class="up-reveal" style="display:none">
<div class="up-field">
<label>تغییرات</label>
<p id="comparesummary-@($"{Model.PageInformation.Poem.Verses[verseIndex].VOrder}")" class="up-field-static">@Model.PageInformation.Poem.Verses[verseIndex].CoupletSummary</p>
</div>
</div>
<a role="button" href="#savecorrections" class="up-btn up-btn--sm up-btn--save-jump up-btn--block">⬇ رفتن به ذخیره</a>
</div>
}
}
</div>
@if (Model.PageInformation.Poem.Sections.Count(s => s.SectionType == RMuseum.Models.Ganjoor.PoemSectionType.WholePoem && s.VerseType == RMuseum.Models.Ganjoor.VersePoemSectionType.First) > 1)
{
<div class="up-card" id="poem-sections">
<h2 class="up-card__title">قطعات</h2>
@foreach (var poemSetion in Model.PageInformation.Poem.Sections.Where(s => s.SectionType == RMuseum.Models.Ganjoor.PoemSectionType.WholePoem && s.VerseType == RMuseum.Models.Ganjoor.VersePoemSectionType.First).ToList())
{
<div class="up-verse-row">
<a href="/User/Section?poemId=@poemSetion.PoemId&index=@poemSetion.Index" role="button" class="up-btn up-btn--sm up-btn--ghost">
🧩 قطعهٔ @(poemSetion.Number.ToPersianNumbers()):
</a>
<p>@Html.Raw(poemSetion.HtmlText)</p>
<p class="up-hint">وزن: @(poemSetion.GanjoorMetre == null ? "تعیین نشده" : poemSetion.GanjoorMetre.Rhythm)</p>
</div>
}
</div>
}
else
if (Model.PageInformation.Poem.Sections.Count(s => s.SectionType == RMuseum.Models.Ganjoor.PoemSectionType.WholePoem && s.VerseType == RMuseum.Models.Ganjoor.VersePoemSectionType.First) == 1)
{
foreach (var poemSetion in Model.PageInformation.Poem.Sections.Where(s => s.SectionType == RMuseum.Models.Ganjoor.PoemSectionType.WholePoem && s.VerseType == RMuseum.Models.Ganjoor.VersePoemSectionType.First).ToList())
{
<p id="poem-sections">
<a href="/User/Section?poemId=@poemSetion.PoemId&index=@poemSetion.Index" role="button" class="up-btn up-btn--ghost">
✂️ شکستن شعر به قطعات مجزا
</a>
</p>
}
}
<p class="up-btn-group-label">قطعات و مصرع‌ها</p>
<div class="up-btn-group">
<a href="/User/AllPoemSections?id=@Model.PageInformation.Id" role="button" class="up-btn up-btn--ghost">
📚 همهٔ قطعات شعر
</a>
<a role="button" href="/User/VerseAdd/?id=@Model.PageInformation.Id" class="up-btn up-btn--ghost">➕ مصرع‌های جاافتاده</a>
</div>
<p class="up-btn-group-label">چینش مصرع‌ها</p>
<div class="up-btn-group">
<a role="button" onclick="resetVersePositions(@Model.PageInformation.Poem.Verses.Length)" class="up-btn up-btn--ghost">🔄 پیش‌فرض</a>
<a role="button" onclick="tarkib3VersePositions(@Model.PageInformation.Poem.Verses.Length)" class="up-btn up-btn--ghost">3️⃣ مثلثی</a>
<a role="button" onclick="tarkib4VersePositions(@Model.PageInformation.Poem.Verses.Length)" class="up-btn up-btn--ghost">4️⃣ مربعی</a>
<a role="button" onclick="tarkib5VersePositions(@Model.PageInformation.Poem.Verses.Length)" class="up-btn up-btn--ghost">5️⃣ مخمسی</a>
<a role="button" onclick="tarkib6VersePositions(@Model.PageInformation.Poem.Verses.Length)" class="up-btn up-btn--ghost">6️⃣ مسدسی</a>
<a role="button" onclick="tarkib7VersePositions(@Model.PageInformation.Poem.Verses.Length)" class="up-btn up-btn--ghost">7️⃣ مسبعی</a>
<a role="button" onclick="tarkib8VersePositions(@Model.PageInformation.Poem.Verses.Length)" class="up-btn up-btn--ghost">8️⃣ مثمنی</a>
</div>
<p class="up-btn-group-label">پیمایش</p>
<div class="up-btn-group">
@if (Model.PageInformation.Poem.Previous != null)
{
@if (Model.ShowAdminOps)
{
<a href="/User/Editor?admin=1&id=@Model.PageInformation.Poem.Previous.Id" role="button" class="up-btn up-btn--ghost">شعر قبل ▶</a>
}
else
{
<a href="/User/Editor?id=@Model.PageInformation.Poem.Previous.Id" role="button" class="up-btn up-btn--ghost">شعر قبل ▶</a>
}
}
@if (Model.PageInformation.Poem.Next != null)
{
@if (Model.ShowAdminOps)
{
<a href="/User/Editor?admin=1&id=@Model.PageInformation.Poem.Next.Id" role="button" class="up-btn up-btn--ghost">◀ شعر بعد</a>
}
else
{
<a href="/User/Editor?id=@Model.PageInformation.Poem.Next.Id" role="button" class="up-btn up-btn--ghost">◀ شعر بعد</a>
}
}
</div>
@if (Model.ShowAdminOps)
{
<p class="up-btn-group-label">ابزارهای مدیریتی</p>
<div class="up-btn-group">
@if (Model.GanjoorMetre1 != null && !string.IsNullOrEmpty(Model.RhymeLetters))
{
<a role="button" onclick="updateRelatedSections(@Model.GanjoorMetre1.Id, '@Model.RhymeLetters')" class="up-btn up-btn--ghost">🔁 هم‌آهنگ‌های وزن اول</a>
}
@if (Model.GanjoorMetre2 != null && !string.IsNullOrEmpty(Model.RhymeLetters))
{
<a role="button" onclick="updateRelatedSections(@Model.GanjoorMetre2.Id, '@Model.RhymeLetters')" class="up-btn up-btn--ghost">🔁 هم‌آهنگ‌های وزن دوم</a>
}
<a role="button" onclick="updateCoupletIndices(@Model.PageInformation.Poem.Id)" class="up-btn up-btn--ghost">🔢 تصحیح اندیس‌های ابیات</a>
<a role="button" onclick="regenerateSections(@Model.PageInformation.Poem.Id)" class="up-btn up-btn--ghost">♻️ قطعه‌بندی مجدد</a>
</div>
}
@if (Model.CanEdit && !Model.ShowAdminOps)
{
<div class="up-btn-group">
<a role="button" href="/User/Editor?id=@Model.PageInformation.Id&admin=1" class="up-btn up-btn--ghost">⚙️ امکانات مدیریتی</a>
</div>
}
@if (Model.ShowAdminOps)
{
<p class="up-hint">برای حذف شعر از قسمت مدیریت بخش اقدام کنید.</p>
}
<div class="up-card" id="geo-date-section">
<h2 class="up-card__title">🗺️ برچسب‌های زمان و مکان (پیشنهادی)</h2>
<p class="up-hint">می‌توانید پیشنهاد دهید که این شعر به مکان یا تاریخ خاصی اشاره دارد. پیشنهادهای شما پس از بررسی ناظران اعمال خواهد شد. با دکمهٔ «ذخیره» بالا همراه با بقیهٔ تغییرات ثبت می‌شود.</p>
@if (Model.PoemGeoDateTags != null && Model.PoemGeoDateTags.Any(t => t.PersonId == null))
{
<table>
<tr>
<th>بیت</th>
<th>مکان</th>
<th>سال</th>
<th>ماه</th>
<th>روز</th>
<th>#</th>
</tr>
@* شخصیت tags are shown in their own section below, not mixed in with geo/date tags here - see #person-tag-section *@
@foreach (var tag in Model.PoemGeoDateTags.Where(t => t.PersonId == null))
{
<tr id="existing-geotag-@tag.Id">
<td>@(Model.Couplets.Where(c => c.Item1 == tag.CoupletIndex).Select(c => c.Item2).FirstOrDefault() ?? "— کل شعر —")</td>
<td>@($"{(tag.Location == null ? "" : tag.Location.Name)}")</td>
<td>@tag.LunarYear</td>
<td>
@{
switch (tag.LunarMonth)
{
case 1:
@("محرم")
;
break;
case 2:
@("صفر")
;
break;
case 3:
@("ربیع‌الاول")
;
break;
case 4:
@("ربیع‌الثانی")
;
break;
case 5:
@("جمادی‌الاول")
;
break;
case 6:
@("جمادی‌الثانی")
;
break;
case 7:
@("رجب")
;
break;
case 8:
@("شعبان")
;
break;
case 9:
@("رمضان")
;
break;
case 10:
@("شوال")
;
break;
case 11:
@("ذیقعده")
;
break;
case 12:
@("ذیحجه")
;
break;
}
}
</td>
<td>@tag.LunarDay</td>
<td>
<a role="button" class="up-btn up-btn--sm up-btn--ghost" onclick="requestDeleteExistingGeoDateTag(@tag.Id, this)">🗑️ درخواست حذف</a>
</td>
</tr>
}
</table>
}
<div id="pending-geotag-suggestions"></div>
<div class="up-field">
<label for="geotag-couplet-select">بیت</label>
<select id="geotag-couplet-select">
<option value="">— کل شعر —</option>
@foreach (var couplet in Model.Couplets)
{
<option value="@couplet.Item1">@($"{(couplet.Item1 + 1).ToPersianNumbers()} - {couplet.Item2}")</option>
}
</select>
<p class="up-hint">اگر این برچسب به یک بیت مشخص اشاره دارد، آن را انتخاب کنید تا دیگران بتوانند شاهد آن را در متن پیدا کنند.</p>
</div>
<div class="up-field">
<label for="geotag-location-search">مکان</label>
<div class="up-autocomplete-wrap">
<input type="text" id="geotag-location-search" autocomplete="off" placeholder="نام مکان را تایپ کنید..." />
<input type="hidden" id="geotag-location-select" />
<div id="geotag-location-results" class="up-autocomplete-results" style="display:none;"></div>
</div>
<p class="up-hint">شروع به تایپ کنید تا فهرست مکان‌های موجود بر اساس نام فیلتر شود؛ اگر مکان مورد نظر پیدا نشد، گزینهٔ تعریف آن به‌عنوان مکان جدید نمایش داده می‌شود.</p>
<a role="button" class="up-btn up-btn--sm up-btn--ghost" onclick="toggleNewLocationWidget()">📍 مکان جدید پیشنهاد بده</a>
</div>
<div id="new-location-widget" style="display:none;">
<div class="up-field">
<label for="geotag-new-location-name">نام مکان پیشنهادی</label>
<input type="text" id="geotag-new-location-name" />
</div>
<div class="up-field-row">
<div class="up-field">
<label for="geotag-new-location-lat">عرض جغرافیایی (Latitude)</label>
<input type="text" id="geotag-new-location-lat" placeholder="مثلاً 32.6546" />
</div>
<div class="up-field">
<label for="geotag-new-location-lng">طول جغرافیایی (Longitude)</label>
<input type="text" id="geotag-new-location-lng" placeholder="مثلاً 51.6680" />
</div>
<a role="button" class="up-btn up-btn--sm up-btn--ghost" onclick="viewNewLocationOnMap()">🗺️ مشاهده روی نقشه</a>
</div>
<p class="up-hint">
برای پیدا کردن مختصات دقیق یک مکان، به‌خصوص مکان‌های کمتر شناخته‌شده، لازم نیست حدس بزنید یا روی نقشهٔ زیر کلیک کنید:
صفحهٔ آن مکان را در ویکی‌پدیا باز کنید؛ نزدیک عنوان صفحه یا در بخش اطلاعات (Infobox) لینکی به مختصات جغرافیایی وجود دارد
(معمولاً به سایت <a href="https://geohack.toolforge.org" target="_blank" rel="noopener">geohack.toolforge.org</a> می‌رود).
عرض جغرافیایی (Latitude / N) و طول جغرافیایی (Longitude / E) را از همان صفحه کپی کنید و در دو فیلد بالا وارد کنید،
سپس با «🗺️ مشاهده روی نقشه» درست بودن نقطه را بررسی کنید.
برای مکان‌های شناخته‌شده‌تر، به‌جای این کار می‌توانید مستقیماً روی نقشهٔ زیر هم کلیک کنید تا مختصات همان‌جا پر شود؛
اما برای مکان‌های کم‌شناخته‌شده روش بالا معمولاً دقیق‌تر و سریع‌تر است.
</p>
<div id="geotag-map" style="height:300px;"></div>
</div>
<div class="up-field-row">
<div class="up-field">
<label for="geotag-year">سال (قمری)</label>
<input type="text" id="geotag-year" />
</div>
<div class="up-field">
<label for="geotag-month">ماه</label>
<select id="geotag-month">
<option value="">هیچکدام</option>
<option value="1">محرم</option>
<option value="2">صفر</option>
<option value="3">ربیع‌الاول</option>
<option value="4">ربیع‌الثانی</option>
<option value="5">جمادی‌الاول</option>
<option value="6">جمادی‌الثانی</option>
<option value="7">رجب</option>
<option value="8">شعبان</option>
<option value="9">رمضان</option>
<option value="10">شوال</option>
<option value="11">ذیقعده</option>
<option value="12">ذیحجه</option>
</select>
</div>
<div class="up-field">
<label for="geotag-day">روز</label>
<input type="text" id="geotag-day" />
</div>
</div>
<div class="up-check-row">
<input type="checkbox" id="geotag-ignore-in-category" />
<label for="geotag-ignore-in-category">در نقشهٔ کلی مجموعه نادیده گرفته شود (مثلاً مکانی که فقط برای مقایسه ذکر شده)</label>
</div>
<div class="up-field">
<label for="geotag-note">دلیل این برچسب (اختیاری، به‌ویژه برای اشاره‌های غیرمستقیم مثل ابجد یا تلمیح به رویدادی شناخته‌شده)</label>
<textarea id="geotag-note" rows="2"></textarea>
</div>
<a role="button" class="up-btn up-btn--ghost" onclick="addGeoDateTagSuggestion()">➕ افزودن به فهرست پیشنهادها</a>
</div>
@* شخصیت (person) tags use the same GanjoorPoemGeoDateTagCorrection structure as the geo/date
tags above (see the JS comment above geoDateTagSuggestions), but a couplet referring to a
place/time and a couplet referring to a person are unrelated claims, so this is its own card
with its own "existing tags" table, its own pending list, and its own add button - a
suggestion coming out of this card never carries a location or a date. *@
<div class="up-card" id="person-tag-section">
<h2 class="up-card__title">🧑‍🎓 برچسب‌های نامبرده (پیشنهادی)</h2>
<p class="up-hint">می‌توانید پیشنهاد دهید که این شعر به یک نامبرده (تاریخی یا داستانی) اشاره دارد. پیشنهادهای شما پس از بررسی ناظران اعمال خواهد شد. با دکمهٔ «ذخیره» بالا همراه با بقیهٔ تغییرات ثبت می‌شود.</p>
@if (Model.PoemGeoDateTags != null && Model.PoemGeoDateTags.Any(t => t.PersonId != null))
{
<table>
<tr>
<th>بیت</th>
<th>نامبرده</th>
<th>#</th>
</tr>
@foreach (var tag in Model.PoemGeoDateTags.Where(t => t.PersonId != null))
{
<tr id="existing-geotag-@tag.Id">
<td>@(Model.Couplets.Where(c => c.Item1 == tag.CoupletIndex).Select(c => c.Item2).FirstOrDefault() ?? "— کل شعر —")</td>
<td>@(tag.Person != null ? tag.Person.Name : $"نامبرده #{tag.PersonId}")</td>
<td>
<a role="button" class="up-btn up-btn--sm up-btn--ghost" onclick="requestDeleteExistingPersonTag(@tag.Id, this)">🗑️ درخواست حذف</a>
</td>
</tr>
}
</table>
}
<div id="pending-persontag-suggestions"></div>
<div class="up-field">
<label for="persontag-couplet-select">بیت</label>
<select id="persontag-couplet-select">
<option value="">— کل شعر —</option>
@foreach (var couplet in Model.Couplets)
{
<option value="@couplet.Item1">@($"{(couplet.Item1 + 1).ToPersianNumbers()} - {couplet.Item2}")</option>
}
</select>
<p class="up-hint">اگر این برچسب به یک بیت مشخص اشاره دارد، آن را انتخاب کنید تا دیگران بتوانند شاهد آن را در متن پیدا کنند.</p>
</div>
<div class="up-field">
<label for="geotag-person-search">نامبرده</label>
<div class="up-autocomplete-wrap">
<input type="text" id="geotag-person-search" autocomplete="off" placeholder="نام نامبرده را تایپ کنید..." />
<input type="hidden" id="geotag-person-select" />
<div id="geotag-person-results" class="up-autocomplete-results" style="display:none;"></div>
</div>
<p class="up-hint">شروع به تایپ کنید تا فهرست نامبردگان بر اساس نام فیلتر شود؛ اگر نامبردۀ مورد نظر پیدا نشد، گزینهٔ تعریف او به‌عنوان نامبردۀ جدید نمایش داده می‌شود.</p>
<a role="button" class="up-btn up-btn--sm up-btn--ghost" onclick="toggleNewPersonWidget()">👤 نامبردۀ جدید پیشنهاد بده</a>
</div>
<div id="new-person-widget" style="display:none;">
<div class="up-field">
<label for="geotag-new-person-name">نام</label>
<input type="text" id="geotag-new-person-name" oninput="updateAllPersonRelativeHints()" />
</div>
<div class="up-field">
<label for="geotag-new-person-description">توضیحات (اختیاری)</label>
<textarea id="geotag-new-person-description" rows="2"></textarea>
</div>
<div class="up-field">
<label for="geotag-new-person-wikiurl">لینک ویکی‌پدیا (اختیاری)</label>
<input type="text" id="geotag-new-person-wikiurl" />
</div>
<div class="up-field">
<label for="geotag-new-person-familytreecaption">عنوان تبارنامه (اختیاری، فقط اگر این نامبرده ریشهٔ یک خانواده/سلسله شناخته‌شده است - مثلاً «ساسانیان»)</label>
<input type="text" id="geotag-new-person-familytreecaption" />
</div>
<div class="up-field">
<label for="geotag-new-person-importance">درجهٔ اهمیت</label>
<select id="geotag-new-person-importance">
<option value="Normal" selected="selected">معمولی</option>
<option value="Important">مهم</option>
<option value="VeryImportant">بسیار مهم</option>
</select>
<p class="up-hint"><small>فقط اندازهٔ نمایش این نامبرده را در نمودار شخصیت‌ها تعیین می‌کند.</small></p>
</div>
<div class="up-field">
<label for="geotag-new-person-gender">جنسیت</label>
<select id="geotag-new-person-gender">
<option value="Unknown">نامشخص</option>
<option value="Male" selected="selected">مرد</option>
<option value="Female">زن</option>
</select>
<p class="up-hint"><small>فقط در ترسیم نمودار تبارنامه استفاده می‌شود (برای تشخیص اینکه کدام والد به عنوان ادامهٔ اصلی شاخهٔ خانوادگی رسم شود).</small></p>
</div>
<p class="up-hint">
اگر خویشاوندی یا وابستگی این نامبرده با نامبردۀ دیگری را می‌دانید، می‌توانید همین‌جا اضافه کنید (نامبردۀ دیگر باید از قبل در فهرست موجود باشد؛
برای نامبرده‌ای که او هم تازه است، ابتدا این فرم را برای او هم به‌صورت جدا تکمیل و ثبت کنید، سپس این رابطه را اضافه کنید).
</p>
<div id="geotag-person-relatives-list"></div>
<a role="button" class="up-btn up-btn--sm up-btn--ghost" onclick="addPersonRelativeRow()">➕ افزودن خویشاوند یا وابستگی</a>
</div>
<div class="up-field">
<label for="persontag-note">دلیل این برچسب (اختیاری، به‌ویژه برای اشاره‌های غیرمستقیم به این نامبرده)</label>
<textarea id="persontag-note" rows="2"></textarea>
</div>
<a role="button" class="up-btn up-btn--ghost" onclick="addPersonTagSuggestion()">➕ افزودن به فهرست پیشنهادها</a>
</div>
<div class="up-card">
<div class="up-check-row">
@if (Model.MyLastEdit != null && Model.MyLastEdit.HideMyName)
{
<input type="checkbox" name="hide-my-name" id="hide-my-name" checked />
}
else
{
<input type="checkbox" name="hide-my-name" id="hide-my-name" />
}
<label for="hide-my-name">مشخصات من در تاریخچهٔ ویرایش‌های این شعر نمایش داده نشود</label>
</div>
<div class="up-form-actions">
<a role="button" id="savecorrections" class="up-btn up-btn--success up-btn--block" onclick="savePoemCorrections(@Model.PageInformation.Poem.Id, @Model.PageInformation.Poem.Verses.Length)">💾 ذخیره</a>
</div>
<p id="saving" class="up-hint">&nbsp;</p>
</div>
@if (Model.ShowAdminOps)
{
<div class="up-card">
<h2 class="up-card__title">برچسب‌های زمان و مکان</h2>
<table>
<tr>
<th>مکان</th>
<th>سال</th>
<th>ماه</th>
<th>روز</th>
<th>تاریخ مستند</th>
<th>مختص شعر</th>
<th>#</th>
</tr>
@foreach (var tag in Model.PoemGeoDateTags)
{
<tr id="tag-@tag.Id">
<td>@($"{(tag.Location == null ? "" : tag.Location.Name)}")</td>
<td>@tag.LunarYear</td>
<td>
@{
switch (tag.LunarMonth)
{
case 1:
@("محرم")
;
break;
case 2:
@("صفر")
;
break;
case 3:
@("ربیع‌الاول")
;
break;
case 4:
@("ربیع‌الثانی")
;
break;
case 5:
@("جمادی‌الاول")
;
break;
case 6:
@("جمادی‌الثانی")
;
break;
case 7:
@("رجب")
;
break;
case 8:
@("شعبان")
;
break;
case 9:
@("رمضان")
;
break;
case 10:
@("شوال")
;
break;
case 11:
@("ذیقعده")
;
break;
case 12:
@("ذیحجه")
;
break;
}
}
</td>
<td>@tag.LunarDay</td>
<td>
@if (tag.VerifiedDate)
{
<input type="checkbox" disabled checked />
}
else
{
<input type="checkbox" disabled />
}
</td>
<td>
@if (tag.IgnoreInCategory)
{
<input type="checkbox" disabled checked />
}
else
{
<input type="checkbox" disabled />
}
</td>
<td>
<a role="button" onclick="deletTag(@tag.Id)" class="actionlink" title="حذف"><i class="noindent-info-button delete-icon"></i></a>
</td>
</tr>
}
<tr>
<td>
<div class="up-autocomplete-wrap">
<input type="text" id="location-search" autocomplete="off" placeholder="نام مکان را تایپ کنید..." />
<input type="hidden" id="location" />
<div id="location-results" class="up-autocomplete-results" style="display:none;"></div>
</div>
</td>
<td>
<input type="text" id="year" />
</td>
<td>
<select id="month">
<option value="0">هیچکدام</option>
<option value="1">محرم</option>
<option value="2">صفر</option>
<option value="3">ربیع‌الاول</option>
<option value="4">ربیع‌الثانی</option>
<option value="5">جمادی‌الاول</option>
<option value="6">جمادی‌الثانی</option>
<option value="7">رجب</option>
<option value="8">شعبان</option>
<option value="9">رمضان</option>
<option value="10">شوال</option>
<option value="11">ذیقعده</option>
<option value="12">ذیحجه</option>
</select>
</td>
<td>
<input type="text" id="day" />
</td>
<td>
<input type="checkbox" id="verifiedDate" />
</td>
<td>
<input type="checkbox" id="ignoreInCategory" />
</td>
<td></td>
</tr>
<tr>
<td colspan="7">
<a role="button" class="up-btn up-btn--sm up-btn--ghost" onclick="newGeoDateTag(@Model.PageInformation.Id)">➕ برچسب جدید</a>
</td>
</tr>
</table>
<a href="/Admin/Locations" role="button" class="up-btn up-btn--sm up-btn--ghost">📍 مکان‌ها</a>
</div>
}
@if (Model.ShowAdminOps)
{
<div class="up-card">
<h2 class="up-card__title">ابرداده‌های اضافه</h2>
<div class="up-check-row">
@if (Model.PageInformation.NoIndex)
{
<input type="checkbox" id="chk_noindex" name="chk_noindex" checked />
}
else
{
<input type="checkbox" id="chk_noindex" name="chk_noindex" />
}
<label for="chk_noindex">ایندکس نشود</label>
</div>
<div class="up-field up-field--ltr">
<label for="redirect_from_url">از این نشانی (کامل) هم به این صفحه ری‌دایرکت شود</label>
<input value="@Model.PageInformation.RedirectFromFullUrl" id="redirect_from_url" name="redirect_from_url" />
</div>
<div class="up-field">
<label for="mixed_mode_order">ترتیب در فهرست‌ها</label>
<input value="@Model.PageInformation.Poem.MixedModeOrder" id="mixed_mode_order" name="mixed_mode_order" />
</div>
<div class="up-form-actions">
<a role="button" class="up-btn up-btn--success" onclick="savemeta(@Model.PageInformation.Id)">💾 ذخیرهٔ ابرداده‌ها</a>
</div>
</div>
}
<p id="footer" class="up-text-center">
<a role="button" href="#" class="up-btn up-btn--sm up-btn--ghost">⬆ بالای صفحه</a>
</p>
}