435 lines
19 KiB
HTML
435 lines
19 KiB
HTML
{% load static %}
|
||
{% load i18n %}
|
||
{% load mastodon %}
|
||
{% load thumb %}
|
||
{% get_current_language as LANGUAGE_CODE %}
|
||
<!DOCTYPE html>
|
||
<html lang="{{ LANGUAGE_CODE }}" class="classic-page">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>{{ site_name }} - {% trans 'Data Management' %}</title>
|
||
{% include "common_libs.html" %}
|
||
<script>
|
||
document.addEventListener('htmx:responseError', (event) => {
|
||
let response = event.detail.xhr.response;
|
||
let body = response ? response : `Request error: ${event.detail.xhr.statusText}`;
|
||
alert(body);
|
||
});
|
||
</script>
|
||
</head>
|
||
<body>
|
||
{% include "_header.html" %}
|
||
<main>
|
||
<div class="grid__main">
|
||
<article>
|
||
<details>
|
||
<summary>{% trans 'Import Marks and Reviews from Douban' %}</summary>
|
||
<form action="{% url 'users:import_douban' %}"
|
||
method="post"
|
||
enctype="multipart/form-data">
|
||
{% csrf_token %}
|
||
{% blocktrans %}Select <code>.xlsx</code> exported from <a href="https://doufen.org" target="_blank" rel="noopener">Doufen</a>{% endblocktrans %}
|
||
<input type="file" name="file" id="excel" required accept=".xlsx">
|
||
<fieldset>
|
||
<legend>{% trans "Import Method" %}</legend>
|
||
<label for="import_mode_0">
|
||
<input id="import_mode_0" type="radio" name="import_mode" value="0" checked>
|
||
{% trans "Merge: only update when status changes from wishlist to in-progress, or from in-progress to complete." %}
|
||
</label>
|
||
<label for="import_mode_1">
|
||
<input id="import_mode_1" type="radio" name="import_mode" value="1">
|
||
{% trans "Overwrite: update all imported status." %}
|
||
</label>
|
||
</fieldset>
|
||
<p>
|
||
{% trans 'Visibility' %}:
|
||
<br>
|
||
<label for="id_visibility_0">
|
||
<input type="radio"
|
||
name="visibility"
|
||
value="0"
|
||
required=""
|
||
id="id_visibility_0"
|
||
checked>
|
||
{% trans 'Public' %}
|
||
</label>
|
||
<label for="id_visibility_1">
|
||
<input type="radio"
|
||
name="visibility"
|
||
value="1"
|
||
required=""
|
||
id="id_visibility_1">
|
||
{% trans 'Followers Only' %}
|
||
</label>
|
||
<label for="id_visibility_2">
|
||
<input type="radio"
|
||
name="visibility"
|
||
value="2"
|
||
required=""
|
||
id="id_visibility_2">
|
||
{% trans 'Mentioned Only' %}
|
||
</label>
|
||
</p>
|
||
<input type="submit"
|
||
{% if import_task.status == "pending" %} onclick="return confirm('{% trans "Another import is in progress, starting a new import may cause issues, sure to import?" %}')" value="{% trans "Import in progress, please wait" %}" {% else %} value="{% trans 'Import' %}" {% endif %} />
|
||
</form>
|
||
</details>
|
||
</article>
|
||
<article>
|
||
<details>
|
||
<summary>{% trans 'Import Shelf or List from Goodreads' %}</summary>
|
||
<form hx-post="{% url 'users:import_goodreads' %}">
|
||
{% csrf_token %}
|
||
<div>
|
||
{% trans 'Link to Goodreads Profile / Shelf / List' %}
|
||
<ul>
|
||
<li>
|
||
Profile <code>https://www.goodreads.com/user/show/12345-janedoe</code>
|
||
<br>
|
||
{% trans 'want-to-read / currently-reading / read books and their reviews will be imported.' %}
|
||
</li>
|
||
<li>
|
||
Shelf <code>https://www.goodreads.com/review/list/12345-janedoe?shelf=name</code>
|
||
<br>
|
||
{% trans 'Shelf will be imported as a new collection.' %}
|
||
</li>
|
||
<li>
|
||
List <code>https://www.goodreads.com/list/show/155086.Popular_Highlights</code>
|
||
<br>
|
||
{% trans 'List will be imported as a new collection.' %}
|
||
</li>
|
||
<li>
|
||
<mark>Who Can View My Profile</mark> must be set as <mark>anyone</mark> prior to import.
|
||
</li>
|
||
</ul>
|
||
<input type="url"
|
||
name="url"
|
||
value=""
|
||
placeholder="https://www.goodreads.com/user/show/12345-janedoe"
|
||
required>
|
||
<input type="submit" value="{% trans 'Import' %}" />
|
||
</div>
|
||
{% include "users/user_task_status.html" with task=goodreads_task %}
|
||
</form>
|
||
</details>
|
||
</article>
|
||
<article>
|
||
<details>
|
||
<summary>{% trans 'Import from Letterboxd' %}</summary>
|
||
<form hx-post="{% url 'users:import_letterboxd' %}"
|
||
enctype="multipart/form-data">
|
||
{% csrf_token %}
|
||
<ul>
|
||
<li>
|
||
In letterboxd.com,
|
||
<a href="https://letterboxd.com/settings/data/"
|
||
target="_blank"
|
||
rel="noopener">click DATA in Settings</a>;
|
||
or in its app, tap Advanced Settings in Settings, tap EXPORT YOUR DATA
|
||
</li>
|
||
<li>
|
||
download file with name like <code>letterboxd-username-2018-03-11-07-52-utc.zip</code>, do not unzip.
|
||
</li>
|
||
</ul>
|
||
<br>
|
||
<input type="file" name="file" required accept=".zip">
|
||
<p>
|
||
{% trans 'Visibility' %}:
|
||
<br>
|
||
<label for="l_visibility_0">
|
||
<input type="radio"
|
||
name="visibility"
|
||
value="0"
|
||
required=""
|
||
id="l_visibility_0"
|
||
checked>
|
||
{% trans 'Public' %}
|
||
</label>
|
||
<label for="l_visibility_1">
|
||
<input type="radio"
|
||
name="visibility"
|
||
value="1"
|
||
required=""
|
||
id="l_visibility_1">
|
||
{% trans 'Followers Only' %}
|
||
</label>
|
||
<label for="l_visibility_2">
|
||
<input type="radio"
|
||
name="visibility"
|
||
value="2"
|
||
required=""
|
||
id="l_visibility_2">
|
||
{% trans 'Mentioned Only' %}
|
||
</label>
|
||
</p>
|
||
<input type="submit" value="{% trans 'Import' %}" />
|
||
<small>{% trans 'Only forward changes(none->to-watch->watched) will be imported.' %}</small>
|
||
{% include "users/user_task_status.html" with task=letterboxd_task %}
|
||
</form>
|
||
</details>
|
||
</article>
|
||
<article>
|
||
<details>
|
||
<summary>{% trans 'Import Podcast Subscriptions' %}</summary>
|
||
<form hx-post="{% url 'users:import_opml' %}" enctype="multipart/form-data">
|
||
{% csrf_token %}
|
||
<div>
|
||
{% trans 'Import Method' %}:
|
||
<label for="opml_import_mode_0">
|
||
<input id="opml_import_mode_0"
|
||
type="radio"
|
||
name="import_mode"
|
||
value="0"
|
||
checked>
|
||
{% trans 'Mark as listening' %}
|
||
</label>
|
||
<label for="opml_import_mode_1">
|
||
<input id="opml_import_mode_1" type="radio" name="import_mode" value="1">
|
||
{% trans 'Import as a new collection' %}
|
||
</label>
|
||
{% trans 'Visibility' %}:
|
||
<label for="opml_visibility_0">
|
||
<input type="radio"
|
||
name="visibility"
|
||
value="0"
|
||
required=""
|
||
id="opml_visibility_0"
|
||
checked>
|
||
{% trans 'Public' %}
|
||
</label>
|
||
<label for="opml_visibility_1">
|
||
<input type="radio"
|
||
name="visibility"
|
||
value="1"
|
||
required=""
|
||
id="opml_visibility_1">
|
||
{% trans 'Followers Only' %}
|
||
</label>
|
||
<label for="opml_visibility_2">
|
||
<input type="radio"
|
||
name="visibility"
|
||
value="2"
|
||
required=""
|
||
id="opml_visibility_2">
|
||
{% trans 'Mentioned Only' %}
|
||
</label>
|
||
<br>
|
||
{% trans 'Select OPML file' %}
|
||
<input type="file" name="file" required accept=".opml,.xml">
|
||
<input type="submit" value="{% trans 'Import' %}" />
|
||
</div>
|
||
{% include "users/user_task_status.html" with task=opml_import_task %}
|
||
</form>
|
||
</details>
|
||
</article>
|
||
<article>
|
||
<details>
|
||
<summary>{% trans 'Import NeoDB Archive' %}</summary>
|
||
<form hx-post="{% url 'users:import_neodb' %}"
|
||
enctype="multipart/form-data">
|
||
{% csrf_token %}
|
||
<ul>
|
||
<li>
|
||
{% trans 'Upload a <code>.zip</code> file containing <code>.csv</code> or <code>.ndjson</code> files exported from NeoDB.' %}
|
||
</li>
|
||
<li>{% trans 'Existing data may be overwritten.' %}</li>
|
||
</ul>
|
||
<input type="file" name="file" id="neodb_import_file" required accept=".zip">
|
||
<div id="detected_format_info"
|
||
style="display: none;
|
||
margin: 10px 0;
|
||
padding: 8px 12px;
|
||
border-radius: 4px;
|
||
background-color: var(--card-background-color, #f8f9fa);
|
||
border: 1px solid var(--card-border-color, #dee2e6)">
|
||
<i class="fa fa-info-circle"></i> {% trans 'Detected format' %}: <strong id="detected_format">-</strong>
|
||
</div>
|
||
<div id="visibility_settings" style="display: none;">
|
||
<p>
|
||
{% trans 'Visibility' %}:
|
||
<br>
|
||
<label for="csv_visibility_0">
|
||
<input type="radio"
|
||
name="visibility"
|
||
value="0"
|
||
required=""
|
||
id="csv_visibility_0"
|
||
checked>
|
||
{% trans 'Public' %}
|
||
</label>
|
||
<label for="csv_visibility_1">
|
||
<input type="radio"
|
||
name="visibility"
|
||
value="1"
|
||
required=""
|
||
id="csv_visibility_1">
|
||
{% trans 'Followers Only' %}
|
||
</label>
|
||
<label for="csv_visibility_2">
|
||
<input type="radio"
|
||
name="visibility"
|
||
value="2"
|
||
required=""
|
||
id="csv_visibility_2">
|
||
{% trans 'Mentioned Only' %}
|
||
</label>
|
||
</p>
|
||
</div>
|
||
<input type="hidden" name="format_type" id="format_type" value="" required>
|
||
<input type="submit" value="{% trans 'Import' %}" id="import_submit" />
|
||
<script src="{{ cdn_url }}/npm/jszip@3.10.1/dist/jszip.min.js"></script>
|
||
<script>
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
const fileInput = document.getElementById('neodb_import_file');
|
||
if (!fileInput) return;
|
||
|
||
fileInput.addEventListener('change', async function(event) {
|
||
const file = event.target.files[0];
|
||
if (!file) {
|
||
document.getElementById('detected_format_info').style.display = 'none';
|
||
document.getElementById('visibility_settings').style.display = 'none';
|
||
document.getElementById('format_type').value = '';
|
||
return;
|
||
}
|
||
|
||
// Check if it's a zip file
|
||
if (file.type !== 'application/zip' &&
|
||
file.type !== 'application/x-zip-compressed' &&
|
||
!file.name.toLowerCase().endsWith('.zip')) {
|
||
document.getElementById('detected_format_info').style.display = 'none';
|
||
document.getElementById('visibility_settings').style.display = 'none';
|
||
document.getElementById('format_type').value = '';
|
||
return;
|
||
}
|
||
|
||
// Update UI to show "Detecting format..." with a spinner
|
||
document.getElementById('detected_format').innerHTML = '{% trans "Detecting format..." %} <i class="fa fa-spinner fa-spin"></i>';
|
||
document.getElementById('detected_format_info').style.display = 'block';
|
||
|
||
try {
|
||
// Use JSZip to examine the actual contents of the ZIP file
|
||
const zip = new JSZip();
|
||
const zipContents = await zip.loadAsync(file);
|
||
const fileNames = Object.keys(zipContents.files);
|
||
|
||
// Check for specific files that indicate format type
|
||
const hasNdjson = fileNames.some(name => name === 'journal.ndjson' || name === 'catalog.ndjson');
|
||
const hasCsv = fileNames.some(name => name.endsWith('_mark.csv') ||
|
||
name.endsWith('_review.csv') ||
|
||
name.endsWith('_note.csv'));
|
||
|
||
let format = '';
|
||
let formatValue = '';
|
||
|
||
if (hasNdjson) {
|
||
format = 'NDJSON';
|
||
formatValue = 'ndjson';
|
||
} else if (hasCsv) {
|
||
format = 'CSV';
|
||
formatValue = 'csv';
|
||
} else {
|
||
// Unable to detect format from contents
|
||
format = '{% trans "Unknown format" %}';
|
||
formatValue = '';
|
||
}
|
||
|
||
// Update UI with detected format and appropriate icon
|
||
let formatIcon = '';
|
||
if (formatValue === 'ndjson') {
|
||
formatIcon = '<i class="fa fa-file-code"></i> ';
|
||
} else if (formatValue === 'csv') {
|
||
formatIcon = '<i class="fa fa-file-csv"></i> ';
|
||
} else {
|
||
formatIcon = '<i class="fa fa-question-circle"></i> ';
|
||
}
|
||
|
||
document.getElementById('detected_format').innerHTML = formatIcon + format;
|
||
document.getElementById('format_type').value = formatValue;
|
||
|
||
if (formatValue === 'csv') {
|
||
document.getElementById('visibility_settings').style.display = 'block';
|
||
} else {
|
||
document.getElementById('visibility_settings').style.display = 'none';
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('Error examining ZIP file:', error);
|
||
document.getElementById('detected_format').innerHTML = '<i class="fa fa-exclamation-triangle"></i> {% trans "Error detecting format" %}';
|
||
document.getElementById('format_type').value = '';
|
||
|
||
// Make the error more visible
|
||
document.getElementById('detected_format_info').style.backgroundColor = 'var(--form-element-invalid-active-border-color, #d9534f)';
|
||
document.getElementById('detected_format_info').style.color = 'white';
|
||
|
||
// Hide visibility settings on error
|
||
document.getElementById('visibility_settings').style.display = 'none';
|
||
}
|
||
if (document.getElementById('format_type').value == '') {
|
||
document.getElementById('import_submit').setAttribute('disabled', '')
|
||
} else {
|
||
document.getElementById('import_submit').removeAttribute('disabled', '')
|
||
}
|
||
});
|
||
});
|
||
</script>
|
||
{% include "users/user_task_status.html" with task=neodb_import_task %}
|
||
</form>
|
||
</details>
|
||
</article>
|
||
<article>
|
||
<details>
|
||
<summary>{% trans 'Export NeoDB Archive' %}</summary>
|
||
<form hx-post="{% url 'users:export_csv' %}" enctype="multipart/form-data">
|
||
{% csrf_token %}
|
||
<input type="submit"
|
||
value="{% trans 'Export marks, reviews and notes in CSV' %}" />
|
||
{% include "users/user_task_status.html" with task=csv_export_task %}
|
||
</form>
|
||
<hr>
|
||
<form hx-post="{% url 'users:export_ndjson' %}"
|
||
enctype="multipart/form-data">
|
||
{% csrf_token %}
|
||
<input type="submit" value="{% trans 'Export everything in NDJSON' %}" />
|
||
{% include "users/user_task_status.html" with task=ndjson_export_task %}
|
||
</form>
|
||
<hr>
|
||
<form action="{% url 'users:export_marks' %}"
|
||
method="post"
|
||
enctype="multipart/form-data">
|
||
{% csrf_token %}
|
||
<b>exporting to this format will be deprecated soon, please use csv or ndjson format.</b>
|
||
<input type="submit"
|
||
class="secondary"
|
||
value="{% trans 'Export marks and reviews in XLSX (Doufen format)' %}" />
|
||
{% if export_task %}
|
||
<br>
|
||
{% trans 'Last export' %}: {{ export_task.created_time }}
|
||
{% trans 'Status' %}: {{ export_task.get_state_display }}
|
||
<br>
|
||
{{ export_task.message }}
|
||
{% if export_task.metadata.file %}
|
||
<a href="{% url 'users:export_marks' %}" download>{% trans 'Download' %}</a>
|
||
{% endif %}
|
||
{% endif %}
|
||
</form>
|
||
</details>
|
||
</article>
|
||
<article>
|
||
<details>
|
||
<summary>{% trans 'View Annual Summary' %}</summary>
|
||
<div class="tag-list">
|
||
{% for year in years %}
|
||
<span>
|
||
<a href="{% url 'journal:wrapped' year %}">{{ year }}</a>
|
||
</span>
|
||
{% endfor %}
|
||
</div>
|
||
</details>
|
||
</article>
|
||
</div>
|
||
{% include "_sidebar.html" with show_profile=1 identity=request.user.identity %}
|
||
</main>
|
||
{% include "_footer.html" %}
|
||
</body>
|
||
</html>
|