wip
This commit is contained in:
parent
b125ecbee6
commit
2a9df13fe3
30
dev/public/js/init.js
Normal file
30
dev/public/js/init.js
Normal file
@ -0,0 +1,30 @@
|
||||
drag.init("app");
|
||||
|
||||
document.addEventListener("alpine:init", () => {
|
||||
// create and init stores
|
||||
Alpine.store("files", filesStore);
|
||||
Alpine.store("lightbox", lightboxStore);
|
||||
Alpine.store("ui", uiStore);
|
||||
Alpine.store("userPrefs", userPrefsStore);
|
||||
|
||||
const salutations = [
|
||||
"Hi!",
|
||||
"Hiya!",
|
||||
"Hello there!",
|
||||
"Good day!",
|
||||
"Hullo!",
|
||||
"Buongiorno!",
|
||||
"Guten Tag!",
|
||||
"Bonjour!",
|
||||
"Oh hey!",
|
||||
];
|
||||
Alpine.store("ui").logMsg(`MARL loaded. ${salutations[Math.floor(Math.random() * salutations.length)]} 😊`);
|
||||
|
||||
resetStores();
|
||||
});
|
||||
|
||||
document.addEventListener("alpine-i18n:ready", function () {
|
||||
AlpineI18n.create("en", appStrings);
|
||||
AlpineI18n.fallbackLocale = "en";
|
||||
setLang();
|
||||
});
|
36
dev/public/js/libs.js
Normal file
36
dev/public/js/libs.js
Normal file
@ -0,0 +1,36 @@
|
||||
const isFileProtocol = window.location.protocol === "file:";
|
||||
|
||||
const scripts = [
|
||||
{
|
||||
src: "js/jszip.min.js",
|
||||
integrity: "sha512-XMVd28F1oH/O71fzwBnV7HucLxVwtxf26XV8P4wPk26EDxuGZ91N8bsOttmnomcCD3CS5ZMRL50H0GgOHvegtg==",
|
||||
crossorigin: "anonymous",
|
||||
defer: false,
|
||||
},
|
||||
// Note: Alpine plug-ins must be inserted BEFORE alpinejs
|
||||
{
|
||||
src: "js/alpinejs-i18n.min.js",
|
||||
integrity: "sha256-o204NcFyHPFzboSC51fufMqFe2KJdQfSCl8AlvSZO/E=",
|
||||
crossorigin: "anonymous",
|
||||
defer: true,
|
||||
},
|
||||
{
|
||||
src: "js/alpinejs.min.js",
|
||||
integrity: "sha512-FUaEyIgi9bspXaH6hUadCwBLxKwdH7CW24riiOqA5p8hTNR/RCLv9UpAILKwqs2AN5WtKB52CqbiePBei3qjKg==",
|
||||
crossorigin: "anonymous",
|
||||
defer: true,
|
||||
},
|
||||
];
|
||||
|
||||
scripts.forEach(({ src, integrity, crossorigin, defer }) => {
|
||||
const script = document.createElement("script");
|
||||
script.src = src;
|
||||
if (!isFileProtocol) {
|
||||
script.integrity = integrity;
|
||||
script.crossOrigin = crossorigin;
|
||||
}
|
||||
if (defer) {
|
||||
script.defer = true;
|
||||
}
|
||||
document.head.appendChild(script);
|
||||
});
|
File diff suppressed because it is too large
Load Diff
888
dev/public/js/stores.js
Normal file
888
dev/public/js/stores.js
Normal file
@ -0,0 +1,888 @@
|
||||
// stores definitions
|
||||
|
||||
const userPrefsStore = {
|
||||
prefix: "marl_",
|
||||
|
||||
save(pref, value) {
|
||||
const msg = `Saving user preference <b>(${pref}: ${value})</b>`;
|
||||
Alpine.store("ui").logMsg(msg, "info");
|
||||
localStorage.setItem(this.prefix + pref, value);
|
||||
},
|
||||
load(pref) {
|
||||
const value = localStorage.getItem(this.prefix + pref);
|
||||
if (value !== null) {
|
||||
this.set(pref, value);
|
||||
} else if (pref === "lang") {
|
||||
this.set(pref, value);
|
||||
}
|
||||
},
|
||||
set(pref, value) {
|
||||
switch (pref) {
|
||||
case "sortAsc":
|
||||
value = +value === 1 ? true : false;
|
||||
if (value !== Alpine.store("files").sortAsc) {
|
||||
Alpine.store("files").sortAsc = value;
|
||||
}
|
||||
break;
|
||||
|
||||
case "pageSize":
|
||||
value = +value;
|
||||
if (typeof value == "number" && !isNaN(value) && value > 0 && value !== Alpine.store("files").pageSize) {
|
||||
Alpine.store("files").pageSize = value;
|
||||
}
|
||||
break;
|
||||
|
||||
case "lang":
|
||||
if (!value) {
|
||||
value = detectLangFromBrowser();
|
||||
if (value) {
|
||||
this.save("lang", value);
|
||||
}
|
||||
}
|
||||
if (!value || !Alpine.store("ui").appLangs[value]) {
|
||||
if (value) {
|
||||
const msg = `<b>Unrecognized language</b> in user preferences: ${value}`;
|
||||
console.warn(msg);
|
||||
Alpine.store("ui").logMsg(msg, "warn");
|
||||
}
|
||||
value = "en";
|
||||
this.save("lang", value);
|
||||
}
|
||||
Alpine.store("ui").lang = value;
|
||||
break;
|
||||
|
||||
case "theme":
|
||||
if (!(value === "dark" || value === "light")) {
|
||||
value = "light";
|
||||
this.save("theme", value);
|
||||
}
|
||||
Alpine.store("ui").theme = value;
|
||||
setTheme(value);
|
||||
break;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const filesStore = {
|
||||
resetState() {
|
||||
this.sources = [];
|
||||
this.toots = [];
|
||||
this.toc = [];
|
||||
this.duplicates = false;
|
||||
|
||||
this.sortAsc = true; // -> userPrefs
|
||||
this.pageSize = 10; // -> userPrefs
|
||||
this.currentPage = 1;
|
||||
|
||||
this.loading = false;
|
||||
this.someFilesLoaded = false;
|
||||
|
||||
this.languages = {};
|
||||
this.boostsAuthors = [];
|
||||
|
||||
this.filters = {};
|
||||
this.filtersDefault = {
|
||||
fullText: "",
|
||||
hashtagText: "",
|
||||
mentionText: "",
|
||||
externalLink: "",
|
||||
summary: "",
|
||||
isEdited: false,
|
||||
isDuplicate: false,
|
||||
noStartingAt: false,
|
||||
hasExternalLink: false,
|
||||
hasHashtags: false,
|
||||
hasMentions: false,
|
||||
hasSummary: false,
|
||||
isSensitive: false,
|
||||
visibilityPublic: true,
|
||||
visibilityUnlisted: true,
|
||||
visibilityFollowers: true,
|
||||
visibilityMentioned: true,
|
||||
typeOriginal: true,
|
||||
typeBoost: true,
|
||||
attachmentAny: false,
|
||||
attachmentImage: false,
|
||||
attachmentVideo: false,
|
||||
attachmentSound: false,
|
||||
attachmentNoAltText: false,
|
||||
attachmentWithAltText: false,
|
||||
|
||||
// automatically generated (see loadJsonFile()):
|
||||
// lang_en: true,
|
||||
// lang_fr: true,
|
||||
// lang_de: true,
|
||||
// etc
|
||||
// actor_0: true,
|
||||
// actor_1: true,
|
||||
// actor_2: true,
|
||||
// etc
|
||||
};
|
||||
this.filtersActive = false;
|
||||
|
||||
this.tagsFilters = {
|
||||
hashtags: "",
|
||||
mentions: "",
|
||||
boostsAuthors: "",
|
||||
};
|
||||
|
||||
Alpine.store("userPrefs").load("sortAsc");
|
||||
Alpine.store("userPrefs").load("pageSize");
|
||||
},
|
||||
|
||||
setFilter() {
|
||||
this.checkPagingValue();
|
||||
scrollTootsToTop();
|
||||
pagingUpdated();
|
||||
if (JSON.stringify(this.filters) === JSON.stringify(this.filtersDefault)) {
|
||||
this.filtersActive = false;
|
||||
} else {
|
||||
this.filtersActive = true;
|
||||
}
|
||||
|
||||
const self = this;
|
||||
setTimeout(() => {
|
||||
self.checkPagingValue();
|
||||
}, 50);
|
||||
},
|
||||
filterByTag(filter, value, id) {
|
||||
if (value) {
|
||||
if (value === this.filters[filter]) {
|
||||
this.filters[filter] = "";
|
||||
} else {
|
||||
this.filters[filter] = value;
|
||||
}
|
||||
}
|
||||
|
||||
// "boosted users" group
|
||||
// in this case let's also (un)check the 'boost type' filters
|
||||
if (filter == "fullText") {
|
||||
if (this.filters[filter] === "") {
|
||||
this.filters.typeBoost = true;
|
||||
this.filters.typeOriginal = true;
|
||||
} else {
|
||||
this.filters.typeBoost = true;
|
||||
this.filters.typeOriginal = false;
|
||||
}
|
||||
}
|
||||
|
||||
this.setFilter();
|
||||
|
||||
// keyboard focus may be lost when tags list changes
|
||||
setTimeout(() => {
|
||||
document.getElementById(id).focus();
|
||||
}, 100);
|
||||
},
|
||||
resetFilters(userAction) {
|
||||
this.filters = JSON.parse(JSON.stringify(this.filtersDefault));
|
||||
if (userAction) {
|
||||
this.currentPage = 1;
|
||||
this.filtersActive = false;
|
||||
scrollTootsToTop();
|
||||
pagingUpdated();
|
||||
}
|
||||
},
|
||||
|
||||
get filteredToots() {
|
||||
const f = this.filters;
|
||||
const fa = this.filtersActive;
|
||||
return this.toots.filter((t) => {
|
||||
if (!fa) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (f.fullText) {
|
||||
let show = false;
|
||||
if (t._marl.textContent) {
|
||||
const filterValue = f.fullText.toLowerCase();
|
||||
|
||||
if (filterValue && t._marl.textContent && t._marl.textContent.indexOf(filterValue) >= 0) {
|
||||
show = true;
|
||||
}
|
||||
}
|
||||
if (!show) {
|
||||
return show;
|
||||
}
|
||||
}
|
||||
|
||||
if (f.hashtagText) {
|
||||
if (typeof t.object === "object" && t.object !== null && t.object.tag) {
|
||||
const filterValue = f.hashtagText.toLowerCase();
|
||||
if (
|
||||
!t.object.tag.some((t) => {
|
||||
return t.type === "Hashtag" && t.name.toLowerCase().indexOf(filterValue) > -1;
|
||||
})
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (f.mentionText) {
|
||||
if (typeof t.object === "object" && t.object !== null && t.object.tag) {
|
||||
const filterValue = f.mentionText.toLowerCase();
|
||||
if (
|
||||
!t.object.tag.some((t) => {
|
||||
return t.type === "Mention" && t.name.toLowerCase().indexOf(filterValue) > -1;
|
||||
})
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (f.summary) {
|
||||
if (t._marl.summary) {
|
||||
const filterValue = f.summary.toLowerCase();
|
||||
if (t._marl.summary.indexOf(filterValue) === -1) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (f.isEdited) {
|
||||
if (!(typeof t.object === "object" && t.object !== null && t.object.updated)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (f.isDuplicate) {
|
||||
if (!t._marl.duplicate) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (f.noStartingAt) {
|
||||
if (!t._marl.textContent || t._marl.textContent.indexOf("@") === 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (f.hasExternalLink) {
|
||||
if (!t._marl.externalLinks || !t._marl.externalLinks.length) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (f.hasHashtags) {
|
||||
if (typeof t.object === "object" && t.object !== null && t.object.tag) {
|
||||
if (
|
||||
!t.object.tag.some((t) => {
|
||||
return t.type === "Hashtag";
|
||||
})
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (f.hasMentions) {
|
||||
if (typeof t.object === "object" && t.object !== null && t.object.tag) {
|
||||
if (
|
||||
!t.object.tag.some((t) => {
|
||||
return t.type === "Mention";
|
||||
})
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (f.hasSummary) {
|
||||
if (typeof t.object === "object" && t.object !== null) {
|
||||
if (!t.object.summary) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (f.isSensitive) {
|
||||
if (typeof t.object === "object" && t.object !== null) {
|
||||
if (!t.object.sensitive) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (f.externalLink) {
|
||||
let show = false;
|
||||
if (t._marl.externalLinks && t._marl.externalLinks.length) {
|
||||
const filterValue = f.externalLink.toLowerCase();
|
||||
show = t._marl.externalLinks.some((link) => {
|
||||
return link.href.indexOf(filterValue) > -1 || link.text.indexOf(filterValue) > -1;
|
||||
});
|
||||
}
|
||||
if (!show) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!f.visibilityPublic && t._marl.visibility[0] === "public") {
|
||||
return false;
|
||||
}
|
||||
if (!f.visibilityUnlisted && t._marl.visibility[0] === "unlisted") {
|
||||
return false;
|
||||
}
|
||||
if (!f.visibilityFollowers && t._marl.visibility[0] === "followers") {
|
||||
return false;
|
||||
}
|
||||
if (!f.visibilityMentioned && t._marl.visibility[0] === "mentioned") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!f.typeOriginal && t.type === "Create") {
|
||||
return false;
|
||||
}
|
||||
if (!f.typeBoost && t.type === "Announce") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (f.attachmentAny) {
|
||||
if (!t._marl.hasAttachments) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (f.attachmentImage) {
|
||||
if (t._marl.hasAttachments) {
|
||||
if (
|
||||
!t.object.attachment.some((att) => {
|
||||
return attachmentIsImage(att);
|
||||
})
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (f.attachmentVideo) {
|
||||
if (t._marl.hasAttachments) {
|
||||
if (
|
||||
!t.object.attachment.some((att) => {
|
||||
return attachmentIsVideo(att);
|
||||
})
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (f.attachmentSound) {
|
||||
if (t._marl.hasAttachments) {
|
||||
if (
|
||||
!t.object.attachment.some((att) => {
|
||||
return attachmentIsSound(att);
|
||||
})
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (f.attachmentNoAltText) {
|
||||
if (t._marl.hasAttachments) {
|
||||
if (
|
||||
!t.object.attachment.some((att) => {
|
||||
return att.name === null;
|
||||
})
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (f.attachmentWithAltText) {
|
||||
if (t._marl.hasAttachments) {
|
||||
if (
|
||||
!t.object.attachment.some((att) => {
|
||||
return att.name;
|
||||
})
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
for (const lang in this.languages) {
|
||||
if (f.hasOwnProperty("lang_" + lang) && f["lang_" + lang] === false) {
|
||||
if (t._marl.langs.includes(lang) || t._marl.langs.length === 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const source of this.sources) {
|
||||
const id = source.id;
|
||||
if (f.hasOwnProperty("actor_" + id) && f["actor_" + id] === false) {
|
||||
if (t._marl.source === id) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
},
|
||||
|
||||
get listHashtags() {
|
||||
return this.listTags("Hashtag");
|
||||
},
|
||||
get listMentions() {
|
||||
return this.listTags("Mention");
|
||||
},
|
||||
listTags(type) {
|
||||
let filterSource = "";
|
||||
switch (type) {
|
||||
case "Mention":
|
||||
filterSource = "mentions";
|
||||
break;
|
||||
case "Hashtag":
|
||||
filterSource = "hashtags";
|
||||
break;
|
||||
}
|
||||
let h = this.filteredToots.reduce((accu, toot) => {
|
||||
if (tootHasTags(toot)) {
|
||||
for (const key in toot.object.tag) {
|
||||
const tag = toot.object.tag[key];
|
||||
if (
|
||||
tag.type &&
|
||||
tag.type === type &&
|
||||
tag.name &&
|
||||
tag.name.toLowerCase().indexOf(this.tagsFilters[filterSource].toLowerCase()) >= 0
|
||||
) {
|
||||
if (
|
||||
accu.some((item) => {
|
||||
return item.name === tag.name;
|
||||
})
|
||||
) {
|
||||
accu.map((item) => {
|
||||
if (item.name === tag.name) {
|
||||
item.nb++;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
accu.push({
|
||||
name: tag.name,
|
||||
href: tag.href,
|
||||
nb: 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return accu;
|
||||
}, []);
|
||||
|
||||
h.sort((a, b) => {
|
||||
if (a.nb === b.nb) {
|
||||
return a.name.localeCompare(b.name);
|
||||
} else {
|
||||
return b.nb - a.nb;
|
||||
}
|
||||
});
|
||||
|
||||
return h;
|
||||
},
|
||||
get listBoostsAuthors() {
|
||||
let r = this.boostsAuthors.reduce((accu, item) => {
|
||||
if (item.name.toLowerCase().indexOf(this.tagsFilters.boostsAuthors.toLowerCase()) >= 0) {
|
||||
accu.push(item);
|
||||
}
|
||||
return accu;
|
||||
}, []);
|
||||
r.sort((a, b) => {
|
||||
if (a.nb === b.nb) {
|
||||
let aHasNoName = a.name.indexOf("? ") === 0;
|
||||
let bHasNoName = b.name.indexOf("? ") === 0;
|
||||
if (aHasNoName && bHasNoName) {
|
||||
return a.name.localeCompare(b.name);
|
||||
} else if (aHasNoName) {
|
||||
return 1;
|
||||
} else if (bHasNoName) {
|
||||
return -1;
|
||||
} else {
|
||||
return a.name.localeCompare(b.name);
|
||||
}
|
||||
} else {
|
||||
if (a.nb === b.nb) {
|
||||
return a.name.localeCompare(b.name);
|
||||
} else {
|
||||
return b.nb - a.nb;
|
||||
}
|
||||
}
|
||||
});
|
||||
return r;
|
||||
},
|
||||
|
||||
get sortedLanguages() {
|
||||
let langs = [];
|
||||
for (const lang in this.languages) {
|
||||
langs.push([lang, this.languages[lang]]);
|
||||
}
|
||||
langs.sort((a, b) => {
|
||||
if (a[0] === "undefined") {
|
||||
return 1;
|
||||
}
|
||||
if (b[0] === "undefined") {
|
||||
return -1;
|
||||
}
|
||||
if (a[1] === b[1]) {
|
||||
return a[0].localeCompare(b[0]);
|
||||
}
|
||||
return b[1] - a[1];
|
||||
});
|
||||
return langs;
|
||||
},
|
||||
|
||||
get appReady() {
|
||||
if (this.sources.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let r = true;
|
||||
for (let i = 0; i < this.sources.length; i++) {
|
||||
const source = this.sources[i];
|
||||
if (
|
||||
!source.loaded.actor ||
|
||||
!source.loaded.avatar ||
|
||||
!source.loaded.header ||
|
||||
!source.loaded.outbox ||
|
||||
!source.loaded.likes ||
|
||||
!source.loaded.bookmarks
|
||||
) {
|
||||
r = false;
|
||||
}
|
||||
}
|
||||
return r;
|
||||
},
|
||||
|
||||
get totalPages() {
|
||||
return Math.ceil(this.filteredToots.length / this.pageSize);
|
||||
},
|
||||
get pagedToots() {
|
||||
if (this.filteredToots) {
|
||||
return this.filteredToots.filter((_, index) => {
|
||||
let start = (this.currentPage - 1) * this.pageSize;
|
||||
let end = this.currentPage * this.pageSize;
|
||||
if (index >= start && index < end) return true;
|
||||
});
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
sortToots() {
|
||||
this.toots.sort((a, b) => {
|
||||
if (this.sortAsc) {
|
||||
return a.published.localeCompare(b.published);
|
||||
} else {
|
||||
return b.published.localeCompare(a.published);
|
||||
}
|
||||
});
|
||||
},
|
||||
toggleTootsOrder() {
|
||||
this.sortAsc = !this.sortAsc;
|
||||
Alpine.store("userPrefs").save("sortAsc", this.sortAsc ? 1 : 0);
|
||||
this.sortToots();
|
||||
scrollTootsToTop();
|
||||
pagingUpdated();
|
||||
},
|
||||
|
||||
setPostsPerPage() {
|
||||
this.checkPagingValue();
|
||||
Alpine.store("userPrefs").save("pageSize", this.pageSize);
|
||||
},
|
||||
checkPagingValue() {
|
||||
if (this.currentPage < 1) {
|
||||
this.currentPage = 1;
|
||||
} else if (this.currentPage > this.totalPages) {
|
||||
this.currentPage = this.totalPages;
|
||||
}
|
||||
},
|
||||
nextPage(setFocusTo) {
|
||||
if (this.currentPage * this.pageSize < this.filteredToots.length) {
|
||||
this.currentPage++;
|
||||
scrollTootsToTop(setFocusTo);
|
||||
pagingUpdated();
|
||||
}
|
||||
},
|
||||
prevPage(setFocusTo) {
|
||||
if (this.currentPage > 1) {
|
||||
this.currentPage--;
|
||||
scrollTootsToTop(setFocusTo);
|
||||
pagingUpdated();
|
||||
}
|
||||
},
|
||||
firstPage(setFocusTo) {
|
||||
this.currentPage = 1;
|
||||
scrollTootsToTop(setFocusTo);
|
||||
pagingUpdated();
|
||||
},
|
||||
lastPage(setFocusTo) {
|
||||
this.currentPage = this.totalPages;
|
||||
scrollTootsToTop(setFocusTo);
|
||||
pagingUpdated();
|
||||
},
|
||||
};
|
||||
|
||||
const lightboxStore = {
|
||||
resetState() {
|
||||
this.show = false;
|
||||
this.data = [];
|
||||
this.source = 0;
|
||||
this.index = 0;
|
||||
this.origin = "";
|
||||
},
|
||||
|
||||
open(toot, index, origin) {
|
||||
this.data = toot.object.attachment;
|
||||
this.source = toot._marl.source;
|
||||
this.show = true;
|
||||
this.index = index;
|
||||
this.origin = origin;
|
||||
document.getElementById("main-section-inner").setAttribute("inert", true);
|
||||
setTimeout(() => {
|
||||
document.getElementById("lightbox").focus();
|
||||
}, 50);
|
||||
},
|
||||
openProfileImg(name, origin, source) {
|
||||
const data = {
|
||||
object: {
|
||||
attachment: [
|
||||
{
|
||||
name: name,
|
||||
url: name,
|
||||
mediaType: Alpine.store("files").sources[source][name].type,
|
||||
},
|
||||
],
|
||||
},
|
||||
_marl: {
|
||||
source: source,
|
||||
},
|
||||
};
|
||||
this.open(data, 0, origin);
|
||||
},
|
||||
close() {
|
||||
const origin = this.origin;
|
||||
this.data = [];
|
||||
this.index = 0;
|
||||
this.show = false;
|
||||
this.origin = "";
|
||||
document.getElementById("main-section-inner").removeAttribute("inert");
|
||||
document.getElementById(origin).focus();
|
||||
},
|
||||
showNext() {
|
||||
this.index++;
|
||||
if (this.index >= this.data.length) {
|
||||
this.index = 0;
|
||||
}
|
||||
if (!attachmentIsImage(this.data[this.index])) {
|
||||
this.showNext();
|
||||
}
|
||||
},
|
||||
showPrev() {
|
||||
this.index--;
|
||||
if (this.index < 0) {
|
||||
this.index = this.data.length - 1;
|
||||
}
|
||||
if (!attachmentIsImage(this.data[this.index])) {
|
||||
this.showPrev();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const uiStore = {
|
||||
log: [],
|
||||
resetState() {
|
||||
this.pagingOptionsVisible = false;
|
||||
this.openMenu = "";
|
||||
this.actorPanel = 0;
|
||||
this.menuIsActive = false;
|
||||
this.lang = "en";
|
||||
this.appLangs = appLangs ?? { en: "English" };
|
||||
this.theme = "light";
|
||||
this.log = this.log ?? [];
|
||||
|
||||
Alpine.store("userPrefs").load("lang");
|
||||
Alpine.store("userPrefs").load("theme");
|
||||
},
|
||||
|
||||
logMsg(msg, type) {
|
||||
type = type ?? "info";
|
||||
const dateOptions = {
|
||||
hour12: false,
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
};
|
||||
const time = new Date().toLocaleTimeString(Alpine.store("ui").lang, dateOptions);
|
||||
let m = {
|
||||
msg: msg,
|
||||
type: type,
|
||||
time: time,
|
||||
};
|
||||
this.log.unshift(m);
|
||||
},
|
||||
|
||||
toggleTheme() {
|
||||
this.theme = this.theme === "light" ? "dark" : "light";
|
||||
Alpine.store("userPrefs").save("theme", this.theme);
|
||||
setTheme(this.theme);
|
||||
},
|
||||
|
||||
togglePagingOptions() {
|
||||
this.pagingOptionsVisible = !this.pagingOptionsVisible;
|
||||
|
||||
if (this.pagingOptionsVisible) {
|
||||
setTimeout(() => {
|
||||
document.getElementById("paging-options").focus();
|
||||
}, 100);
|
||||
}
|
||||
},
|
||||
get pagingOptionsClass() {
|
||||
return this.pagingOptionsVisible ? "open" : "";
|
||||
},
|
||||
|
||||
openActorPanel(id) {
|
||||
this.actorPanel = id;
|
||||
},
|
||||
switchActorPanel(dir) {
|
||||
let id = this.actorPanel;
|
||||
if (dir === "up") {
|
||||
id++;
|
||||
if (id >= Alpine.store("files").sources.length) {
|
||||
id = 0;
|
||||
}
|
||||
} else {
|
||||
id--;
|
||||
if (id < 0) {
|
||||
id = Alpine.store("files").sources.length - 1;
|
||||
}
|
||||
}
|
||||
this.actorPanel = id;
|
||||
document.getElementById("actortab-" + id).focus();
|
||||
},
|
||||
|
||||
menuClose() {
|
||||
const name = this.openMenu;
|
||||
this.openMenu = "";
|
||||
this.setInert();
|
||||
|
||||
// bring focus back to where it was before the panel was opened
|
||||
document.querySelector("#main-section-inner .mobile-menu .menu-" + name).focus();
|
||||
},
|
||||
menuOpen(name) {
|
||||
this.openMenu = name;
|
||||
this.resetPanels();
|
||||
this.setInert();
|
||||
|
||||
setTimeout(() => {
|
||||
document.getElementById("panel-" + name).focus();
|
||||
}, 100);
|
||||
},
|
||||
menuToggle(name) {
|
||||
switch (name) {
|
||||
case "actor":
|
||||
case "filters":
|
||||
case "tags":
|
||||
case "tools":
|
||||
if (this.openMenu === name) {
|
||||
this.menuClose();
|
||||
} else {
|
||||
this.menuOpen(name);
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
resetPanels() {
|
||||
const name = this.openMenu;
|
||||
document.querySelectorAll(`#panel-${name} details[open]`).forEach((e) => {
|
||||
e.removeAttribute("open");
|
||||
});
|
||||
setTimeout(() => {
|
||||
document.getElementById("panel-" + name).scrollTop = 0;
|
||||
}, 250);
|
||||
},
|
||||
checkMenuState() {
|
||||
const menu = document.getElementById("mobile-menu");
|
||||
if (window.getComputedStyle(menu, null).display === "none") {
|
||||
this.menuIsActive = false;
|
||||
} else {
|
||||
this.menuIsActive = true;
|
||||
}
|
||||
|
||||
this.setInert();
|
||||
},
|
||||
|
||||
setInertMain() {
|
||||
document
|
||||
.querySelectorAll("#main-section-inner > *:not(.mobile-menu, .panel-backdrop, #panel-" + this.openMenu)
|
||||
.forEach((e) => {
|
||||
e.setAttribute("inert", true);
|
||||
});
|
||||
},
|
||||
setInertPanels() {
|
||||
document.querySelectorAll("#panel-actor, #panel-filters, #panel-tags, #panel-tools").forEach((e) => {
|
||||
e.setAttribute("inert", true);
|
||||
});
|
||||
},
|
||||
setInertTools() {
|
||||
document.querySelectorAll("#panel-tools").forEach((e) => {
|
||||
e.setAttribute("inert", true);
|
||||
});
|
||||
},
|
||||
setInert() {
|
||||
// set the 'inert' state on the side panels or the main part of the app
|
||||
// depending on whether they are hidden or not, AND whether the mobile
|
||||
// menu is active
|
||||
|
||||
document.querySelectorAll("#main-section-inner > *").forEach((e) => {
|
||||
e.removeAttribute("inert");
|
||||
});
|
||||
|
||||
if (this.menuIsActive) {
|
||||
if (this.openMenu) {
|
||||
this.setInertMain();
|
||||
} else {
|
||||
this.setInertPanels();
|
||||
}
|
||||
} else {
|
||||
if (this.openMenu === "tools") {
|
||||
this.setInertMain();
|
||||
} else {
|
||||
this.setInertTools();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
get appClasses() {
|
||||
let classes = [];
|
||||
if (this.openMenu) {
|
||||
classes.push("menu-open menu-open-" + this.openMenu);
|
||||
} else {
|
||||
classes.push("menu-closed");
|
||||
}
|
||||
return classes;
|
||||
},
|
||||
};
|
@ -16,7 +16,6 @@ const appStrings = {
|
||||
p3: `<strong>Start by opening your archive file with MARL.</strong><br />
|
||||
You can drag and drop it anywhere on this page, or
|
||||
{labelStart}click here to select it{labelEnd}.`,
|
||||
projectPage: `Project page (github)`,
|
||||
},
|
||||
misc: {
|
||||
loading: "Loading",
|
||||
@ -27,8 +26,7 @@ const appStrings = {
|
||||
filters: "Filters",
|
||||
filtersActive: "some filters are active",
|
||||
tags: "Tags",
|
||||
newFile: "New File",
|
||||
newFileConfirm: "Discard current data and load a new archive file?",
|
||||
tools: "Tools",
|
||||
},
|
||||
lightbox: {
|
||||
next: "Next image",
|
||||
@ -50,8 +48,8 @@ const appStrings = {
|
||||
cache. Posts that are not in your instance cache any more are not included in your
|
||||
archive. This affects boosts, likes, and bookmarks.`,
|
||||
rawData: "Raw data {fileName}",
|
||||
favorites: "Favorites",
|
||||
favoritesEmpty: "no favorites",
|
||||
likes: "Favorites",
|
||||
likesEmpty: "no favorites",
|
||||
bookmarks: "Bookmarks",
|
||||
bookmarksEmpty: "no bookmarks",
|
||||
},
|
||||
@ -140,6 +138,21 @@ const appStrings = {
|
||||
mentionsFilter: "Filter mentions",
|
||||
boostsFilter: "Filter boosted users",
|
||||
},
|
||||
tools: {
|
||||
panelTitle: "Tools",
|
||||
appSettings: "App settings",
|
||||
selectLanguage: "Select language",
|
||||
useDarkTheme: "Use dark theme",
|
||||
useLightTheme: "Use light theme",
|
||||
loadedFiles: "Loaded files",
|
||||
addAnother: "Add another archive",
|
||||
addAnotherTip:
|
||||
"Tip: You can open multiple archives at once.<br>You can also drag and drop your archive files anywhere on this window.",
|
||||
startOver: "Start over",
|
||||
startOverConfirm: "Discard current data and load a new archive file?",
|
||||
appLog: "App log",
|
||||
projectPage: `Project page (github)`,
|
||||
},
|
||||
},
|
||||
|
||||
fr: {
|
||||
@ -154,7 +167,6 @@ const appStrings = {
|
||||
p3: `<strong>Commencez par ouvrir votre archive avec MARL.</strong><br />
|
||||
Vous pouvez la glisser-déposer n'importe où sur cette page, ou
|
||||
{labelStart}cliquer ici pour la sélectionner{labelEnd}.`,
|
||||
projectPage: `Page du project (github)`,
|
||||
},
|
||||
misc: {
|
||||
loading: "Chargement",
|
||||
@ -165,8 +177,7 @@ const appStrings = {
|
||||
filters: "Filtres",
|
||||
filtersActive: "certains filtres sont actifs",
|
||||
tags: "Tags",
|
||||
newFile: "Nouveau fichier",
|
||||
newFileConfirm: "Repartir de zéro et charger un nouveau fichier ?",
|
||||
tools: "Outils",
|
||||
},
|
||||
lightbox: {
|
||||
next: "Image suivante",
|
||||
@ -188,8 +199,8 @@ const appStrings = {
|
||||
de ce cache. Les posts qui ne sont plus présents dans le cache de votre instance ne sont
|
||||
pas inclus dans votre archive. Cela concerne les partages, les favoris et les marque-pages.`,
|
||||
rawData: "Données brutes {fileName}",
|
||||
favorites: "Favoris",
|
||||
favoritesEmpty: "aucun favori",
|
||||
likes: "Favoris",
|
||||
likesEmpty: "aucun favori",
|
||||
bookmarks: "Marque-pages",
|
||||
bookmarksEmpty: "aucun marque-page",
|
||||
},
|
||||
@ -278,5 +289,20 @@ const appStrings = {
|
||||
mentionsFilter: "Filtrer les mentions",
|
||||
boostsFilter: "Filter utilisateurs partagés",
|
||||
},
|
||||
tools: {
|
||||
panelTitle: "Outils",
|
||||
appSettings: "Réglages de l'app",
|
||||
selectLanguage: "Choisir la langue",
|
||||
useDarkTheme: "Utiliser le thème sombre",
|
||||
useLightTheme: "Utiliser le thème clair",
|
||||
loadedFiles: "Fichiers chargés",
|
||||
addAnother: "Ajouter une autre archive",
|
||||
addAnotherTip:
|
||||
"Astuce: Vous pouvez ouvrir plusieurs archives en même temps.<br>Vous pouvez aussi glisser-déposer vos fichiers d'archive n'importe où dans cette fenêtre.",
|
||||
startOver: "Recommencer",
|
||||
startOverConfirm: "Repartir de zéro et charger un nouveau fichier ?",
|
||||
appLog: "Journal",
|
||||
projectPage: `Page du project (github)`,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
678
dev/public/js/utils.js
Normal file
678
dev/public/js/utils.js
Normal file
@ -0,0 +1,678 @@
|
||||
function resetStores() {
|
||||
Alpine.store("files").resetState();
|
||||
Alpine.store("lightbox").resetState();
|
||||
Alpine.store("ui").resetState();
|
||||
}
|
||||
|
||||
function unZip(files) {
|
||||
const firstLoad = Alpine.store("files").sources.length === 0;
|
||||
if (firstLoad) {
|
||||
resetStores();
|
||||
}
|
||||
Alpine.store("files").loading = true;
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
|
||||
if (
|
||||
Alpine.store("files").sources.some((source) => {
|
||||
return (
|
||||
source.fileInfos.name === file.name &&
|
||||
source.fileInfos.size === file.size &&
|
||||
source.fileInfos.lastModified === file.lastModified
|
||||
);
|
||||
})
|
||||
) {
|
||||
const msg = `File already loaded: <b>${file.name}</b>`;
|
||||
console.warn(msg);
|
||||
Alpine.store("ui").logMsg(msg, "warn");
|
||||
continue;
|
||||
}
|
||||
|
||||
Alpine.store("ui").logMsg(`Loading file: <b>${file.name}</b>`, "info");
|
||||
|
||||
JSZip.loadAsync(file).then(
|
||||
(content) => {
|
||||
const index = Alpine.store("files").sources.length;
|
||||
const fileInfos = {
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
lastModified: file.lastModified,
|
||||
};
|
||||
|
||||
Alpine.store("files").sources[index] = {
|
||||
id: index,
|
||||
fileInfos: fileInfos,
|
||||
nbToots: 0,
|
||||
|
||||
actor: {},
|
||||
outbox: {},
|
||||
likes: [],
|
||||
bookmarks: [],
|
||||
avatar: {},
|
||||
header: {},
|
||||
|
||||
loaded: {
|
||||
actor: false,
|
||||
avatar: false,
|
||||
header: false,
|
||||
outbox: false,
|
||||
likes: false,
|
||||
bookmarks: false,
|
||||
},
|
||||
};
|
||||
|
||||
Alpine.store("files").sources[index]._raw = content.files;
|
||||
|
||||
loadJsonFile("actor", index, fileInfos);
|
||||
loadJsonFile("outbox", index, fileInfos);
|
||||
loadJsonFile("likes", index, fileInfos);
|
||||
loadJsonFile("bookmarks", index, fileInfos);
|
||||
},
|
||||
(error) => {
|
||||
const msg = `Error loading <b>${file.name}</b>: ${error.message}`;
|
||||
console.error(msg);
|
||||
Alpine.store("ui").logMsg(msg, "error");
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function loadJsonFile(name, index, fileInfos) {
|
||||
const content = Alpine.store("files").sources[index]._raw;
|
||||
|
||||
if (content[name + ".json"] === undefined) {
|
||||
if (name === "likes" || name === "bookmarks") {
|
||||
// we can still run the app without those files
|
||||
const msg = `<b>${fileInfos.name}</b>: File ${name}.json not found in archive.`;
|
||||
console.warn(msg);
|
||||
Alpine.store("ui").logMsg(msg, "warn");
|
||||
Alpine.store("files").sources[index].loaded[name] = true;
|
||||
} else {
|
||||
// this should NOT happen and will prevent the app from running
|
||||
const msg = `<b>Critical error - ${fileInfos.name}</b>: File ${name}.json not found in archive.`;
|
||||
console.error(msg);
|
||||
Alpine.store("ui").logMsg(msg, "error");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
content[name + ".json"].async("text").then(function (txt) {
|
||||
if (name === "actor") {
|
||||
Alpine.store("files").sources[index].actor = JSON.parse(txt);
|
||||
loadActorImages(index);
|
||||
Alpine.store("files").sources[index].loaded.actor = true;
|
||||
} // actor.json
|
||||
|
||||
if (name === "outbox") {
|
||||
let data = JSON.parse(txt);
|
||||
|
||||
let toots = data.orderedItems.reduce((accu, t) => {
|
||||
let t2 = preprocessToots(t, index);
|
||||
if (t2) {
|
||||
accu.push(t2);
|
||||
}
|
||||
return accu;
|
||||
}, []);
|
||||
|
||||
Alpine.store("files").toots = Alpine.store("files").toots.concat(toots);
|
||||
Alpine.store("files").sources[index].nbToots = toots.length;
|
||||
delete data.orderedItems;
|
||||
Alpine.store("files").sources[index].outbox = data;
|
||||
Alpine.store("files").sources[index].loaded.outbox = true;
|
||||
} // outbox.json
|
||||
|
||||
if (name === "likes" || name === "bookmarks") {
|
||||
const tmp = JSON.parse(txt);
|
||||
Alpine.store("files").sources[index][name] = tmp.orderedItems;
|
||||
Alpine.store("files").sources[index].loaded[name] = true;
|
||||
} // likes.json || bookmarks.json
|
||||
});
|
||||
}
|
||||
|
||||
function buildTootsInfos() {
|
||||
let langs = {};
|
||||
let boosts = [];
|
||||
|
||||
if (Alpine.store("files").toots.length > 0) {
|
||||
let infos = Alpine.store("files").toots.reduce(
|
||||
(accu, toot) => {
|
||||
for (let lang in toot._marl.langs) {
|
||||
const l = toot._marl.langs[lang];
|
||||
if (!accu.langs[l]) {
|
||||
accu.langs[l] = 1;
|
||||
} else {
|
||||
accu.langs[l]++;
|
||||
}
|
||||
}
|
||||
|
||||
if (toot.type === "Announce") {
|
||||
// since Mastodon doesn't allow (yet?) cross-origin requests to
|
||||
// retrieve post data (for boosts), we try to at least extract the
|
||||
// user names for all the boosts contained in the archive
|
||||
|
||||
// [ISSUE] "object" value is a string most of the times, but
|
||||
// sometimes it's a complex object similar to type "Create"
|
||||
if (typeof toot.object === "object" && toot.object !== null) {
|
||||
// let's ignore this case for now...
|
||||
// [TODO], but not clear how it should be handled
|
||||
} else if (toot.object) {
|
||||
// if it's not an object and it has a value, then it's simply a
|
||||
// url (string) pointing to the original (boosted) post.
|
||||
// [ISSUE] URL format not always consistent... (esp. in the case
|
||||
// of non-Mastodon instances) - e.g:
|
||||
// https://craftopi.art/objects/[...]
|
||||
// https://firefish.city/notes/[...]
|
||||
// https://bsky.brid.gy/convert/ap/at://did:plc:[...]/app.bsky.feed.post/[...]
|
||||
// -> the user name is not always present in URL
|
||||
const url = toot.object.split("/");
|
||||
let name;
|
||||
let user;
|
||||
let domain;
|
||||
if (url.length > 2) {
|
||||
domain = url[2];
|
||||
|
||||
if (url[0] === "https:" && url[3] === "users" && url[5] === "statuses") {
|
||||
// Mastodon URL format -> user name
|
||||
name = url[4];
|
||||
user = `https://${url[2]}/users/${url[4]}/`;
|
||||
} else {
|
||||
// other URL format -> domain name
|
||||
name = `? ${url[2]}`;
|
||||
user = `https://${url[2]}/`;
|
||||
}
|
||||
|
||||
if (!accu.boosts[name]) {
|
||||
accu.boosts[name] = {
|
||||
nb: 1,
|
||||
name: name,
|
||||
url: user,
|
||||
domain: domain,
|
||||
};
|
||||
} else {
|
||||
accu.boosts[name].nb++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return accu;
|
||||
},
|
||||
{ langs: {}, boosts: {} }
|
||||
);
|
||||
|
||||
langs = infos.langs;
|
||||
|
||||
boosts = [];
|
||||
for (var key in infos.boosts) {
|
||||
boosts.push(infos.boosts[key]);
|
||||
}
|
||||
}
|
||||
|
||||
Alpine.store("files").languages = langs;
|
||||
Alpine.store("files").boostsAuthors = boosts;
|
||||
}
|
||||
|
||||
function buildDynamicFilters() {
|
||||
for (const lang in Alpine.store("files").languages) {
|
||||
Alpine.store("files").filtersDefault["lang_" + lang] = true;
|
||||
}
|
||||
|
||||
for (const source of Alpine.store("files").sources) {
|
||||
Alpine.store("files").filtersDefault["actor_" + source.id] = true;
|
||||
}
|
||||
|
||||
Alpine.store("files").resetFilters(false);
|
||||
}
|
||||
|
||||
function preprocessToots(t, index) {
|
||||
// build the '_marl' prop for each toot
|
||||
let marl = {
|
||||
langs: [],
|
||||
source: index,
|
||||
};
|
||||
|
||||
// check for duplicates (in case of multiple archive files)
|
||||
if (Alpine.store("files").toc.includes(t.id)) {
|
||||
const alts = Alpine.store("files").toots.filter((t2) => t2.id === t.id);
|
||||
|
||||
let identical = false;
|
||||
const flat1 = JSON.stringify(t);
|
||||
|
||||
alts.forEach((alt) => {
|
||||
let alt2 = JSON.parse(JSON.stringify(alt));
|
||||
delete alt2._marl;
|
||||
const flat2 = JSON.stringify(alt2);
|
||||
|
||||
if (flat1 === flat2) {
|
||||
identical = true;
|
||||
} else {
|
||||
alt._marl.duplicate = true;
|
||||
marl.duplicate = true;
|
||||
Alpine.store("files").duplicates = true;
|
||||
}
|
||||
});
|
||||
if (identical) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
Alpine.store("files").toc.push(t.id);
|
||||
}
|
||||
|
||||
if (t.type === "Create") {
|
||||
if (typeof t.object === "object" && t.object !== null && t.object.contentMap) {
|
||||
let langs = [];
|
||||
for (let lang in t.object.contentMap) {
|
||||
langs.push(lang);
|
||||
}
|
||||
marl.langs = langs;
|
||||
} else {
|
||||
marl.langs = ["undefined"];
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof t.object === "object" && t.object !== null) {
|
||||
if (t.object.content) {
|
||||
const content = t.object.content.toLowerCase();
|
||||
marl.textContent = stripHTML(content);
|
||||
marl.externalLinks = extractExternalLinks(content);
|
||||
}
|
||||
if (t.object.summary) {
|
||||
marl.summary = t.object.summary.toLowerCase();
|
||||
}
|
||||
|
||||
if (t.object.attachment && t.object.attachment.length) {
|
||||
marl.hasAttachments = true;
|
||||
}
|
||||
} else if (t.object) {
|
||||
marl.textContent = t.object.toLowerCase();
|
||||
}
|
||||
|
||||
marl.visibility = tootVisibility(t);
|
||||
|
||||
const id = t.id.split("/");
|
||||
marl.id = id[id.length - 2];
|
||||
|
||||
t._marl = marl;
|
||||
return t;
|
||||
}
|
||||
|
||||
function loadActorImages(index) {
|
||||
const actor = Alpine.store("files").sources[index].actor;
|
||||
const content = Alpine.store("files").sources[index]._raw;
|
||||
|
||||
if (actor.icon && actor.icon.type === "Image" && actor.icon.url && content[actor.icon.url]) {
|
||||
const image = actor.icon;
|
||||
content[image.url].async("base64").then(function (content) {
|
||||
Alpine.store("files").sources[index].avatar = {
|
||||
type: image.mediaType,
|
||||
content: content,
|
||||
noImg: false,
|
||||
};
|
||||
Alpine.store("files").sources[index].loaded.avatar = true;
|
||||
});
|
||||
} else {
|
||||
Alpine.store("files").sources[index].avatar = { noImg: true };
|
||||
Alpine.store("files").sources[index].loaded.avatar = true;
|
||||
}
|
||||
|
||||
if (actor.image && actor.image.type === "Image" && actor.image.url && content[actor.image.url]) {
|
||||
const image = actor.image;
|
||||
content[image.url].async("base64").then(function (content) {
|
||||
Alpine.store("files").sources[index].header = {
|
||||
type: image.mediaType,
|
||||
content: content,
|
||||
noImg: false,
|
||||
};
|
||||
Alpine.store("files").sources[index].loaded.header = true;
|
||||
});
|
||||
} else {
|
||||
Alpine.store("files").sources[index].header = { noImg: true };
|
||||
Alpine.store("files").sources[index].loaded.header = true;
|
||||
}
|
||||
}
|
||||
|
||||
function setHueForSources() {
|
||||
const nbSources = Alpine.store("files").sources.length;
|
||||
const hueStart = Math.round(Math.random() * 360); // MARL accent: 59.17
|
||||
const hueSpacing = Math.round(360 / nbSources);
|
||||
|
||||
for (let i = 0; i < nbSources; i++) {
|
||||
Alpine.store("files").sources[i].hue = hueStart + hueSpacing * i;
|
||||
}
|
||||
}
|
||||
|
||||
function checkAppReady(ok) {
|
||||
if (ok) {
|
||||
buildTootsInfos();
|
||||
buildDynamicFilters();
|
||||
cleanUpRaw();
|
||||
setHueForSources();
|
||||
document.getElementById("main-section").focus();
|
||||
Alpine.store("ui").checkMenuState();
|
||||
Alpine.store("files").sortToots();
|
||||
Alpine.store("files").loading = false;
|
||||
Alpine.store("files").someFilesLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
function cleanUpRaw() {
|
||||
for (let i = 0; i < Alpine.store("files").sources.length; i++) {
|
||||
const content = Alpine.store("files").sources[i]._raw;
|
||||
if (content.cleanedUp) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const actor = Alpine.store("files").sources[i].actor;
|
||||
if (actor.image && actor.image.url) {
|
||||
delete content[actor.image.url];
|
||||
}
|
||||
if (actor.icon && actor.icon.url) {
|
||||
delete content[actor.icon.url];
|
||||
}
|
||||
delete content["actor.json"];
|
||||
delete content["outbox.json"];
|
||||
delete content["likes.json"];
|
||||
delete content["bookmarks.json"];
|
||||
content.cleanedUp = true;
|
||||
|
||||
Alpine.store("files").sources[i]._raw = content;
|
||||
}
|
||||
}
|
||||
|
||||
function loadAttachedMedia(att, index) {
|
||||
if (attachmentIsImage(att) || attachmentIsVideo(att) || attachmentIsSound(att)) {
|
||||
const data = Alpine.store("files").sources[index]._raw;
|
||||
let url = att.url;
|
||||
// ?! some instances seem to add their own name in front of the path,
|
||||
// resulting in an invalid path with relation to the archive
|
||||
// structure (e.g. "/framapiaf/media_attachments/...", but in the
|
||||
// archive there is only a folder "/media_attachments")
|
||||
// => So we remove everything that comes before "media_attachments/",
|
||||
// hoping it doesn't break something else... :/
|
||||
const prefix = url.indexOf("media_attachments/");
|
||||
if (prefix > 0) {
|
||||
url = url.slice(prefix);
|
||||
}
|
||||
if (!data[url]) {
|
||||
return;
|
||||
}
|
||||
data[url].async("base64").then((content) => {
|
||||
Alpine.store("files").sources[index][att.url] = {
|
||||
type: att.mediaType,
|
||||
content: content,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function pagingUpdated() {
|
||||
document.querySelectorAll(`#toots details[open]`).forEach((e) => {
|
||||
e.removeAttribute("open");
|
||||
});
|
||||
}
|
||||
|
||||
function scrollTootsToTop(setFocusTo) {
|
||||
setTimeout(() => {
|
||||
document.getElementById("toots").scrollTop = 0;
|
||||
if (setFocusTo) {
|
||||
// for keyboard users: we transfer the focus to the corresponding button
|
||||
// in the upper paging module; or, in the cases where said button is
|
||||
// disabled, we set the focus on the list of posts.
|
||||
document.getElementById(setFocusTo).focus();
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
|
||||
function contentType(data) {
|
||||
let r = "";
|
||||
switch (data) {
|
||||
case "Create":
|
||||
r = "Post";
|
||||
break;
|
||||
case "Announce":
|
||||
r = "Boost";
|
||||
break;
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
function tootVisibility(data) {
|
||||
if (data.to.includes("https://www.w3.org/ns/activitystreams#Public")) {
|
||||
return ["public", AlpineI18n.t("filters.visibilityPublic")];
|
||||
}
|
||||
if (
|
||||
data.to.some((x) => x.indexOf("/followers") > -1) &&
|
||||
!data.to.includes("https://www.w3.org/ns/activitystreams#Public") &&
|
||||
data.cc.includes("https://www.w3.org/ns/activitystreams#Public")
|
||||
) {
|
||||
return ["unlisted", AlpineI18n.t("filters.visibilityUnlisted")];
|
||||
}
|
||||
if (
|
||||
data.to.some((x) => x.indexOf("/followers") > -1) &&
|
||||
!data.to.includes("https://www.w3.org/ns/activitystreams#Public") &&
|
||||
!data.cc.includes("https://www.w3.org/ns/activitystreams#Public")
|
||||
) {
|
||||
return ["followers", AlpineI18n.t("filters.visibilityFollowers")];
|
||||
}
|
||||
if (
|
||||
!data.to.some((x) => x.indexOf("/followers") > -1) &&
|
||||
!data.to.includes("https://www.w3.org/ns/activitystreams#Public") &&
|
||||
!data.cc.includes("https://www.w3.org/ns/activitystreams#Public")
|
||||
) {
|
||||
return ["mentioned", AlpineI18n.t("filters.visibilityMentioned")];
|
||||
}
|
||||
}
|
||||
|
||||
function tootHasTags(toot) {
|
||||
return typeof toot.object === "object" && toot.object !== null && toot.object.tag && toot.object.tag.length;
|
||||
}
|
||||
|
||||
function formatJson(data) {
|
||||
let r = data;
|
||||
if (r._marl) {
|
||||
// not a part of the source data; let's hide it to avoid confusion
|
||||
r = JSON.parse(JSON.stringify(data));
|
||||
delete r._marl;
|
||||
}
|
||||
return JSON.stringify(r, null, 4);
|
||||
}
|
||||
|
||||
function formatAuthor(author, plainText) {
|
||||
if (plainText) {
|
||||
return author.split("/").pop();
|
||||
} else {
|
||||
return `<a href="${author}" target="_blank">${author.split("/").pop()}</a>`;
|
||||
}
|
||||
}
|
||||
|
||||
function formatDateTime(data) {
|
||||
let date = new Date(data);
|
||||
const dateOptions = {
|
||||
weekday: "long",
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
};
|
||||
return date.toLocaleDateString(Alpine.store("ui").lang, dateOptions);
|
||||
}
|
||||
|
||||
function formatFileDateTime(data) {
|
||||
let date = new Date(data);
|
||||
const dateOptions = {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour12: false,
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
};
|
||||
return date.toLocaleDateString(Alpine.store("ui").lang, dateOptions);
|
||||
}
|
||||
function formatFileSize(size) {
|
||||
var i = size == 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024));
|
||||
return +(size / Math.pow(1024, i)).toFixed(2) * 1 + " " + ["B", "kB", "MB", "GB", "TB"][i]; // ### i18n
|
||||
}
|
||||
|
||||
function formatDate(data) {
|
||||
let date = new Date(data);
|
||||
const dateOptions = {
|
||||
weekday: "long",
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
};
|
||||
return date.toLocaleDateString(Alpine.store("ui").lang, dateOptions);
|
||||
}
|
||||
|
||||
function formatNumber(nb) {
|
||||
return nb.toLocaleString();
|
||||
}
|
||||
|
||||
function formatLikesBookmarks(url) {
|
||||
const u = url.split("/");
|
||||
u.splice(0, 2);
|
||||
|
||||
// 0 [domain]
|
||||
// 1 "users"
|
||||
// 2 [username]
|
||||
// 3 "statuses"
|
||||
// 4 [post id]
|
||||
|
||||
let text = `<span class="url-instance">${u[0]}</span>`;
|
||||
if (u[1] === "users" && u[3] === "statuses") {
|
||||
text += `<span class="url-actor">${u[2]}</span><span class="url-post-id">${u[4]}</span>`;
|
||||
} else {
|
||||
u.splice(0, 1);
|
||||
text += `<span class="url-post-id">${u.join("/")}</span>`;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
function stripHTML(str) {
|
||||
let doc = new DOMParser().parseFromString(str, "text/html");
|
||||
return doc.body.textContent || "";
|
||||
}
|
||||
|
||||
function extractExternalLinks(str) {
|
||||
const doc = new DOMParser().parseFromString(str, "text/html");
|
||||
const nodes = doc.querySelectorAll("a[href]:not(.mention)");
|
||||
let links = [];
|
||||
nodes.forEach((link) => {
|
||||
links.push({
|
||||
href: link.href,
|
||||
text: link.textContent,
|
||||
});
|
||||
});
|
||||
return links;
|
||||
}
|
||||
|
||||
function attachmentIsImage(att) {
|
||||
return att.mediaType === "image/jpeg" || att.mediaType === "image/png";
|
||||
}
|
||||
|
||||
function attachmentIsVideo(att) {
|
||||
return att.mediaType === "video/mp4";
|
||||
}
|
||||
|
||||
function attachmentIsSound(att) {
|
||||
return att.mediaType === "audio/mpeg";
|
||||
}
|
||||
|
||||
function attachmentWrapperClass(att) {
|
||||
let r = [];
|
||||
if (attachmentIsImage(att)) {
|
||||
r.push("att-img");
|
||||
} else if (attachmentIsSound(att)) {
|
||||
r.push("att-sound");
|
||||
} else if (attachmentIsVideo(att)) {
|
||||
r.push("att-video");
|
||||
}
|
||||
|
||||
if (!att.name) {
|
||||
r.push("no-alt-text");
|
||||
}
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
function isFilterActive(name) {
|
||||
return Alpine.store("files").filters[name] !== Alpine.store("files").filtersDefault[name];
|
||||
}
|
||||
|
||||
function startOver() {
|
||||
const txt = AlpineI18n.t("tools.startOverConfirm");
|
||||
if (confirm(txt)) {
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
function detectLangFromBrowser() {
|
||||
const langs = navigator.languages;
|
||||
if (langs && langs.length) {
|
||||
for (let i = 0; i < langs.length; i++) {
|
||||
let lang = langs[i].split("-")[0];
|
||||
if (Alpine.store("ui").appLangs[lang]) {
|
||||
const msg = `Setting language based on browser preference: <b>'${lang}' (${
|
||||
Alpine.store("ui").appLangs[lang]
|
||||
})</b>`;
|
||||
Alpine.store("ui").logMsg(msg, "info");
|
||||
return lang;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function setLang() {
|
||||
const lang = Alpine.store("ui").lang;
|
||||
AlpineI18n.locale = lang;
|
||||
Alpine.store("userPrefs").save("lang", lang);
|
||||
|
||||
const msg = `App language set to <b>'${lang}' (${Alpine.store("ui").appLangs[lang]})</b>`;
|
||||
Alpine.store("ui").logMsg(msg);
|
||||
}
|
||||
|
||||
function setTheme(theme) {
|
||||
document.getElementsByTagName("html")[0].setAttribute("class", theme);
|
||||
}
|
||||
|
||||
// drag'n'drop over entire page
|
||||
|
||||
const drag = {
|
||||
el: null,
|
||||
|
||||
init(el) {
|
||||
this.dropArea = document.getElementById(el);
|
||||
|
||||
["dragenter", "dragover", "dragleave", "drop"].forEach((eventName) => {
|
||||
this.dropArea.addEventListener(eventName, (e) => this.preventDragDefaults(e), false);
|
||||
});
|
||||
["dragenter", "dragover"].forEach((eventName) => {
|
||||
this.dropArea.addEventListener(eventName, () => this.highlightDrag(), false);
|
||||
});
|
||||
["dragleave", "drop"].forEach((eventName) => {
|
||||
this.dropArea.addEventListener(eventName, () => this.unhighlightDrag(), false);
|
||||
});
|
||||
this.dropArea.addEventListener("drop", (e) => this.handleDrop(e), false);
|
||||
},
|
||||
|
||||
preventDragDefaults(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
},
|
||||
highlightDrag() {
|
||||
this.dropArea.classList.add("highlight-drag");
|
||||
},
|
||||
unhighlightDrag() {
|
||||
this.dropArea.classList.remove("highlight-drag");
|
||||
},
|
||||
handleDrop(e) {
|
||||
const dt = e.dataTransfer;
|
||||
const files = dt.files;
|
||||
unZip(files);
|
||||
},
|
||||
};
|
@ -29,9 +29,9 @@
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button class="menu-new" @click="startOver">
|
||||
<svg aria-hidden="true"><use href="#menu-new" /></svg>
|
||||
<span x-text="$t('menu.newFile')"></span>
|
||||
<button class="menu-tools" @click="$store.ui.menuToggle('tools')">
|
||||
<svg aria-hidden="true"><use href="#tools" /></svg>
|
||||
<span x-text="$t('menu.tools')"></span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -6,8 +6,8 @@
|
||||
>
|
||||
<svg class="btn-icon" aria-hidden="true">
|
||||
<use href="#nav-first" />
|
||||
</svg>
|
||||
<span class="btn-label" x-text="$t('paging.first')"></span>
|
||||
</svg
|
||||
><span class="btn-label" x-text="$t('paging.first')"></span>
|
||||
</button>
|
||||
<button
|
||||
@click="$store.files.prevPage('paging-btn-prev')"
|
||||
@ -15,8 +15,8 @@
|
||||
>
|
||||
<svg class="btn-icon" aria-hidden="true">
|
||||
<use href="#nav-prev" />
|
||||
</svg>
|
||||
<span class="btn-label" x-text="$t('paging.prev')"></span>
|
||||
</svg
|
||||
><span class="btn-label" x-text="$t('paging.prev')"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="direction-fwd">
|
||||
@ -24,8 +24,8 @@
|
||||
@click="$store.files.nextPage('paging-btn-next')"
|
||||
:disabled="$store.files.currentPage >= $store.files.totalPages"
|
||||
>
|
||||
<span class="btn-label" x-text="$t('paging.next')"></span>
|
||||
<svg class="btn-icon" aria-hidden="true">
|
||||
<span class="btn-label" x-text="$t('paging.next')"></span
|
||||
><svg class="btn-icon" aria-hidden="true">
|
||||
<use href="#nav-next" />
|
||||
</svg>
|
||||
</button>
|
||||
@ -33,8 +33,8 @@
|
||||
@click="$store.files.lastPage('toots')"
|
||||
:disabled="$store.files.currentPage >= $store.files.totalPages"
|
||||
>
|
||||
<span class="btn-label" x-text="$t('paging.last')"></span>
|
||||
<svg class="btn-icon" aria-hidden="true">
|
||||
<span class="btn-label" x-text="$t('paging.last')"></span
|
||||
><svg class="btn-icon" aria-hidden="true">
|
||||
<use href="#nav-last" />
|
||||
</svg>
|
||||
</button>
|
||||
|
@ -8,11 +8,10 @@
|
||||
min="1"
|
||||
max="$store.files.totalPages"
|
||||
x-model="$store.files.currentPage"
|
||||
@keyup="$store.files.checkPagingValue()"
|
||||
@change="$store.files.checkPagingValue()"
|
||||
onclick="this.select()"
|
||||
/>
|
||||
/
|
||||
/>
|
||||
/
|
||||
<span class="total-pages" x-text="formatNumber($store.files.totalPages)"></span>
|
||||
(
|
||||
<input
|
||||
@ -21,10 +20,9 @@
|
||||
type="number"
|
||||
min="1"
|
||||
x-model="$store.files.pageSize"
|
||||
@keyup="$store.files.checkPagingValue()"
|
||||
@change="$store.files.checkPagingValue()"
|
||||
@change="$store.files.setPostsPerPage()"
|
||||
onclick="this.select()"
|
||||
/>
|
||||
/>
|
||||
<label for="paging-page-size" x-text="$t('paging.postsPerPage')"></label>)
|
||||
</div>
|
||||
<div class="paging-options-reverse">
|
||||
|
@ -11,8 +11,8 @@ import PagingOptions from "./PagingOptions.astro";
|
||||
>
|
||||
<svg class="btn-icon" aria-hidden="true">
|
||||
<use href="#nav-first" />
|
||||
</svg>
|
||||
<span class="btn-label" x-text="$t('paging.first')"></span>
|
||||
</svg
|
||||
><span class="btn-label" x-text="$t('paging.first')"></span>
|
||||
</button>
|
||||
<button
|
||||
id="paging-btn-prev"
|
||||
@ -21,8 +21,8 @@ import PagingOptions from "./PagingOptions.astro";
|
||||
>
|
||||
<svg class="btn-icon" aria-hidden="true">
|
||||
<use href="#nav-prev" />
|
||||
</svg>
|
||||
<span class="btn-label" x-text="$t('paging.prev')"></span>
|
||||
</svg
|
||||
><span class="btn-label" x-text="$t('paging.prev')"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -30,8 +30,8 @@ import PagingOptions from "./PagingOptions.astro";
|
||||
<button @click="$store.ui.togglePagingOptions()">
|
||||
<svg class="btn-icon" aria-hidden="true">
|
||||
<use href="#options" />
|
||||
</svg>
|
||||
<span class="btn-label" x-text="$t('paging.pagingOptions')"></span>
|
||||
</svg
|
||||
><span class="btn-label" x-text="$t('paging.pagingOptions')"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -43,8 +43,8 @@ import PagingOptions from "./PagingOptions.astro";
|
||||
@click="$store.files.nextPage()"
|
||||
:disabled="$store.files.currentPage >= $store.files.totalPages"
|
||||
>
|
||||
<span class="btn-label" x-text="$t('paging.next')"></span>
|
||||
<svg class="btn-icon" aria-hidden="true">
|
||||
<span class="btn-label" x-text="$t('paging.next')"></span
|
||||
><svg class="btn-icon" aria-hidden="true">
|
||||
<use href="#nav-next" />
|
||||
</svg>
|
||||
</button>
|
||||
@ -53,8 +53,8 @@ import PagingOptions from "./PagingOptions.astro";
|
||||
@click="$store.files.lastPage()"
|
||||
:disabled="$store.files.currentPage >= $store.files.totalPages"
|
||||
>
|
||||
<span class="btn-label" x-text="$t('paging.last')"></span>
|
||||
<svg class="btn-icon" aria-hidden="true">
|
||||
<span class="btn-label" x-text="$t('paging.last')"></span
|
||||
><svg class="btn-icon" aria-hidden="true">
|
||||
<use href="#nav-last" />
|
||||
</svg>
|
||||
</button>
|
||||
|
@ -89,14 +89,39 @@
|
||||
d="M478-240q21 0 35.5-14.5T528-290q0-21-14.5-35.5T478-340q-21 0-35.5 14.5T428-290q0 21 14.5 35.5T478-240Zm-36-154h74q0-33 7.5-52t42.5-52q26-26 41-49.5t15-56.5q0-56-41-86t-97-30q-57 0-92.5 30T342-618l66 26q5-18 22.5-39t53.5-21q32 0 48 17.5t16 38.5q0 20-12 37.5T506-526q-44 39-54 59t-10 73Zm38 314q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z"
|
||||
/>
|
||||
</symbol>
|
||||
<symbol viewBox="0 -960 960 960" id="bookmark">
|
||||
<symbol viewBox="0 -960 960 960" id="bookmarks">
|
||||
<path
|
||||
d="M200-120v-640q0-33 23.5-56.5T280-840h400q33 0 56.5 23.5T760-760v640L480-240 200-120Zm80-122 200-86 200 86v-518H280v518Zm0-518h400-400Z"
|
||||
/>
|
||||
</symbol>
|
||||
<symbol viewBox="0 -960 960 960" id="favorite">
|
||||
<symbol viewBox="0 -960 960 960" id="likes">
|
||||
<path
|
||||
d="m354-287 126-76 126 77-33-144 111-96-146-13-58-136-58 135-146 13 111 97-33 143ZM233-120l65-281L80-590l288-25 112-265 112 265 288 25-218 189 65 281-247-149-247 149Zm247-350Z"
|
||||
/>
|
||||
</symbol>
|
||||
<symbol viewBox="0 -960 960 960" id="home">
|
||||
<path
|
||||
d="M240-200h120v-200q0-17 11.5-28.5T400-440h160q17 0 28.5 11.5T600-400v200h120v-360L480-740 240-560v360Zm-80 0v-360q0-19 8.5-36t23.5-28l240-180q21-16 48-16t48 16l240 180q15 11 23.5 28t8.5 36v360q0 33-23.5 56.5T720-120H560q-17 0-28.5-11.5T520-160v-200h-80v200q0 17-11.5 28.5T400-120H240q-33 0-56.5-23.5T160-200Zm320-270Z"
|
||||
/>
|
||||
</symbol>
|
||||
<symbol viewBox="0 -960 960 960" id="dark">
|
||||
<path
|
||||
d="M480-120q-151 0-255.5-104.5T120-480q0-138 90-239.5T440-838q13-2 23 3.5t16 14.5q6 9 6.5 21t-7.5 23q-17 26-25.5 55t-8.5 61q0 90 63 153t153 63q31 0 61.5-9t54.5-25q11-7 22.5-6.5T819-479q10 5 15.5 15t3.5 24q-14 138-117.5 229T480-120Zm0-80q88 0 158-48.5T740-375q-20 5-40 8t-40 3q-123 0-209.5-86.5T364-660q0-20 3-40t8-40q-78 32-126.5 102T200-480q0 116 82 198t198 82Zm-10-270Z"
|
||||
/>
|
||||
</symbol>
|
||||
<symbol viewBox="0 -960 960 960" id="light">
|
||||
<path
|
||||
d="M480-360q50 0 85-35t35-85q0-50-35-85t-85-35q-50 0-85 35t-35 85q0 50 35 85t85 35Zm0 80q-83 0-141.5-58.5T280-480q0-83 58.5-141.5T480-680q83 0 141.5 58.5T680-480q0 83-58.5 141.5T480-280ZM80-440q-17 0-28.5-11.5T40-480q0-17 11.5-28.5T80-520h80q17 0 28.5 11.5T200-480q0 17-11.5 28.5T160-440H80Zm720 0q-17 0-28.5-11.5T760-480q0-17 11.5-28.5T800-520h80q17 0 28.5 11.5T920-480q0 17-11.5 28.5T880-440h-80ZM480-760q-17 0-28.5-11.5T440-800v-80q0-17 11.5-28.5T480-920q17 0 28.5 11.5T520-880v80q0 17-11.5 28.5T480-760Zm0 720q-17 0-28.5-11.5T440-80v-80q0-17 11.5-28.5T480-200q17 0 28.5 11.5T520-160v80q0 17-11.5 28.5T480-40ZM226-678l-43-42q-12-11-11.5-28t11.5-29q12-12 29-12t28 12l42 43q11 12 11 28t-11 28q-11 12-27.5 11.5T226-678Zm494 495-42-43q-11-12-11-28.5t11-27.5q11-12 27.5-11.5T734-282l43 42q12 11 11.5 28T777-183q-12 12-29 12t-28-12Zm-42-495q-12-11-11.5-27.5T678-734l42-43q11-12 28-11.5t29 11.5q12 12 12 29t-12 28l-43 42q-12 11-28 11t-28-11ZM183-183q-12-12-12-29t12-28l43-42q12-11 28.5-11t27.5 11q12 11 11.5 27.5T282-226l-42 43q-11 12-28 11.5T183-183Zm297-297Z"
|
||||
/>
|
||||
</symbol>
|
||||
<symbol viewBox="0 -960 960 960" id="langs">
|
||||
<path
|
||||
d="m603-202-34 97q-4 11-14 18t-22 7q-20 0-32.5-16.5T496-133l152-402q5-11 15-18t22-7h30q12 0 22 7t15 18l152 403q8 19-4 35.5T868-80q-13 0-22.5-7T831-106l-34-96H603ZM362-401 188-228q-11 11-27.5 11.5T132-228q-11-11-11-28t11-28l174-174q-35-35-63.5-80T190-640h84q20 39 40 68t48 58q33-33 68.5-92.5T484-720H80q-17 0-28.5-11.5T40-760q0-17 11.5-28.5T80-800h240v-40q0-17 11.5-28.5T360-880q17 0 28.5 11.5T400-840v40h240q17 0 28.5 11.5T680-760q0 17-11.5 28.5T640-720h-76q-21 72-63 148t-83 116l96 98-30 82-122-125Zm266 129h144l-72-204-72 204Z"
|
||||
/>
|
||||
</symbol>
|
||||
<symbol viewBox="0 -960 960 960" id="tools">
|
||||
<path
|
||||
d="M360-360q-100 0-170-70t-70-170q0-20 3-40t11-38q5-10 12.5-15t16.5-7q9-2 18.5.5T199-689l105 105 72-72-105-105q-8-8-10.5-17.5T260-797q2-9 7-16.5t15-12.5q18-8 38-11t40-3q100 0 170 70t70 170q0 23-4 43.5T584-516l202 200q29 29 29 71t-29 71q-29 29-71 29t-71-30L444-376q-20 8-40.5 12t-43.5 4Zm0-80q26 0 52-8t47-25l243 243q5 5 13.5 4.5T729-231q5-5 5-13.5t-5-13.5L486-500q18-20 26-46.5t8-53.5q0-60-38.5-104.5T386-758l74 74q12 12 12 28t-12 28L332-500q-12 12-28 12t-28-12l-74-74q9 57 53.5 95.5T360-440Zm109-51Z"
|
||||
/>
|
||||
</symbol>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 12 KiB |
@ -5,24 +5,24 @@ import PagingTop from '../PagingTop.astro';
|
||||
<header class="toots-header" aria-labelledby="toots-header-title">
|
||||
<h2 id="toots-header-title">
|
||||
<span class="count">
|
||||
<span x-text="await formatNumber($store.files.filteredToots.length)" class="nb"></span>
|
||||
<span x-text="formatNumber($store.files.filteredToots.length)" class="nb"></span>
|
||||
<span x-text="$t('header.countLabel')"></span>
|
||||
</span>
|
||||
<span class="order">
|
||||
-
|
||||
<span x-text="await $store.files.sortAsc ? $t('header.oldestFirst') : $t('header.latestFirst')"></span>
|
||||
<span x-text="$store.files.sortAsc ? $t('header.oldestFirst') : $t('header.latestFirst')"></span>
|
||||
<button @click="$store.files.toggleTootsOrder()" class="toggle-order">
|
||||
<svg class="btn-icon" aria-hidden="true">
|
||||
<use href="#toggle-order" />
|
||||
</svg>
|
||||
<span class="btn-label" x-text="$t('header.reverse')"></span>
|
||||
</svg
|
||||
><span class="btn-label" x-text="$t('header.reverse')"></span>
|
||||
</button>
|
||||
</span>
|
||||
|
||||
<button @click="startOver" class="load-new">
|
||||
<button class="open-tools" @click="$store.ui.menuToggle('tools')">
|
||||
<svg class="btn-icon" aria-hidden="true">
|
||||
<use href="#load-file" /></svg
|
||||
><span class="btn-label" x-text="$t('header.loadNewFile')"></span>
|
||||
<use href="#tools" /></svg
|
||||
><span class="btn-label" x-text="$t('menu.tools')"></span>
|
||||
</button>
|
||||
</h2>
|
||||
|
||||
|
@ -1,59 +1,29 @@
|
||||
---
|
||||
import CloseBtn from './CloseBtn.astro'
|
||||
import AppSettings from './tools/AppSettings.astro'
|
||||
import ManageFiles from './tools/ManageFiles.astro'
|
||||
import AppLog from './tools/AppLog.astro'
|
||||
import About from './tools/About.astro'
|
||||
---
|
||||
|
||||
<aside
|
||||
class="panel-tools mobile-menu-panel"
|
||||
id="panel-tools"
|
||||
class="tools panel-tools mobile-menu-panel"
|
||||
aria-labelledby="panel-tools-title"
|
||||
tabindex="-1"
|
||||
inert
|
||||
>
|
||||
<CloseBtn />
|
||||
|
||||
<h2 class="tools-title visually-hidden" id="panel-tools-title">Tools</h2>
|
||||
<h2
|
||||
id="panel-tools-title"
|
||||
class="tools-section tools-title visually-hidden"
|
||||
x-text="$t('tools.panelTitle')"
|
||||
></h2>
|
||||
|
||||
<select
|
||||
@change="setLang"
|
||||
x-model="$store.ui.lang"
|
||||
>
|
||||
<template x-for="(lang, id) in $store.ui.appLangs">
|
||||
<option
|
||||
:value="id"
|
||||
x-text="lang"
|
||||
:selected="$store.ui.lang === id"
|
||||
></option>
|
||||
</template>
|
||||
</select>
|
||||
<AppSettings />
|
||||
<ManageFiles />
|
||||
<AppLog />
|
||||
<About />
|
||||
|
||||
<select>
|
||||
<option value="dark">Dark</option>
|
||||
</select>
|
||||
|
||||
<div class="loaded-files">
|
||||
<h3>Loaded files</h3>
|
||||
<ul>
|
||||
<li>File 1</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<button @click="startOver" class="load-new">
|
||||
<svg class="btn-icon" aria-hidden="true">
|
||||
<use href="#load-file" /></svg
|
||||
><span class="btn-label">Load new file</span>
|
||||
</button>
|
||||
|
||||
<div class="app-log">
|
||||
<ul>
|
||||
<li>Message 4 amet consectetur adipisicing elit</li>
|
||||
<li>Message 3 amet consectetur adipisicing elit</li>
|
||||
<li>Message 2 Lorem ipsum dolor sit ipsum dolor sit amet consectetur adipisicing elit</li>
|
||||
<li>Message 1 Lorem ipsum dolor sit amet consectetur adipisicing elit</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="about">
|
||||
<a href="https://github.com/s427/MARL">
|
||||
<span x-text="$t('welcome.projectPage')"></span>
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
|
@ -7,7 +7,7 @@ const { type } = Astro.props;
|
||||
<summary>
|
||||
<span class="summary-icon">
|
||||
<svg aria-hidden="true">
|
||||
<use href="#favorite" />
|
||||
<use href={`#${type}`} />
|
||||
</svg>
|
||||
</span>
|
||||
<h2 class="summary-label">
|
||||
|
7
dev/src/components/panels/tools/About.astro
Normal file
7
dev/src/components/panels/tools/About.astro
Normal file
@ -0,0 +1,7 @@
|
||||
<div class="tools-section about">
|
||||
<p>
|
||||
MARL v. 2.1.0 — <a href="https://github.com/s427/MARL">
|
||||
<span x-text="$t('tools.projectPage')"></span>
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
13
dev/src/components/panels/tools/AppLog.astro
Normal file
13
dev/src/components/panels/tools/AppLog.astro
Normal file
@ -0,0 +1,13 @@
|
||||
<div class="tools-section app-log">
|
||||
<h3 x-text="$t('tools.appLog')"></h3>
|
||||
<ul>
|
||||
<template
|
||||
x-for="m in $store.ui.log"
|
||||
>
|
||||
<li :class="m.type">
|
||||
<span class="time"><time x-text="m.time"></time></span>
|
||||
<span class="msg" x-html="m.msg"></span>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</div>
|
39
dev/src/components/panels/tools/AppSettings.astro
Normal file
39
dev/src/components/panels/tools/AppSettings.astro
Normal file
@ -0,0 +1,39 @@
|
||||
<div class="tools-section app-settings">
|
||||
<h3 x-text="$t('tools.appSettings')"></h3>
|
||||
|
||||
<div class="app-setting switch-lang">
|
||||
<label for="switch-language">
|
||||
<svg aria-hidden="true">
|
||||
<use href="#langs" /></svg>
|
||||
<span class="visually-hidden" x-text="$t('tools.selectLanguage')"></span>
|
||||
</label>
|
||||
<select
|
||||
id="switch-language"
|
||||
@change="setLang"
|
||||
x-model="$store.ui.lang"
|
||||
>
|
||||
<template x-for="(lang, id) in $store.ui.appLangs">
|
||||
<option
|
||||
:value="id"
|
||||
x-text="lang"
|
||||
:selected="$store.ui.lang === id"
|
||||
></option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="app-setting switch-theme">
|
||||
<button @click="$store.ui.toggleTheme()"
|
||||
class="switch-theme"
|
||||
:class="'theme-' + $store.ui.theme"
|
||||
>
|
||||
<svg class="btn-icon" aria-hidden="true">
|
||||
<use
|
||||
:href="'#' + ($store.ui.theme === 'dark' ? 'light' : 'dark')"
|
||||
/></svg
|
||||
><span class="btn-label dark" x-text="$t('tools.useDarkTheme')"></span
|
||||
><span class="btn-label light" x-text="$t('tools.useLightTheme')"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
44
dev/src/components/panels/tools/ManageFiles.astro
Normal file
44
dev/src/components/panels/tools/ManageFiles.astro
Normal file
@ -0,0 +1,44 @@
|
||||
<div class="tools-section manage-files">
|
||||
<div
|
||||
class="loaded-files"
|
||||
:class="$store.files.sources.length > 1 ? 'multiple-files' : 'single-file'"
|
||||
x-show="$store.files.sources.length"
|
||||
>
|
||||
<h3 x-text="$t('tools.loadedFiles')"></h3>
|
||||
<ul class="loaded-files-list">
|
||||
<template
|
||||
x-for="s in $store.files.sources"
|
||||
>
|
||||
<li :style="'--actor-hue: '+ s.hue">
|
||||
<span class="name" x-text="s.fileInfos.name"></span>
|
||||
<span class="date" x-text="formatFileDateTime(s.fileInfos.lastModified)"></span>
|
||||
<span class="size" x-text="formatFileSize(s.fileInfos.size)"></span>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="manage-files-actions">
|
||||
<label for="file-loader2" class="btn load-more">
|
||||
<svg class="btn-icon" aria-hidden="true">
|
||||
<use href="#load-file" /></svg
|
||||
><span class="btn-label" x-text="$t('tools.addAnother')"></span>
|
||||
<input
|
||||
id="file-loader2"
|
||||
class="file-loader visually-hidden"
|
||||
type="file"
|
||||
accept=".zip"
|
||||
multiple
|
||||
@change="unZip(Object.values($event.target.files))"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<button @click="startOver">
|
||||
<svg class="btn-icon" aria-hidden="true">
|
||||
<use href="#home" /></svg
|
||||
><span class="btn-label" x-text="$t('tools.startOver')"></span>
|
||||
</button>
|
||||
|
||||
<div class="tip" x-html="$t('tools.addAnotherTip')"></div>
|
||||
</div>
|
||||
</div>
|
@ -1,5 +1,6 @@
|
||||
---
|
||||
import ToolsPanel from '../panels/ToolsPanel.astro';
|
||||
import AppSettings from '../panels/tools/AppSettings.astro';
|
||||
import About from '../panels/tools/About.astro';
|
||||
---
|
||||
|
||||
<main class="welcome">
|
||||
@ -19,6 +20,8 @@ import ToolsPanel from '../panels/ToolsPanel.astro';
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ToolsPanel />
|
||||
|
||||
<div class="tools">
|
||||
<AppSettings />
|
||||
<About />
|
||||
</div>
|
||||
</main>
|
||||
|
@ -1,3 +1,5 @@
|
||||
@use "mixins";
|
||||
|
||||
.actor {
|
||||
position: relative;
|
||||
&::before {
|
||||
@ -144,13 +146,9 @@
|
||||
color: var(--actor-fg0);
|
||||
line-height: 1.3;
|
||||
background: var(--actor-bg4);
|
||||
background: radial-gradient(
|
||||
circle at 200% 150%,
|
||||
var(--actor-bg3),
|
||||
var(--actor-bg4)
|
||||
);
|
||||
background: radial-gradient(circle at 200% 150%, var(--actor-bg3), var(--actor-bg4));
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 0.3rem 0.4rem -0.2rem rgba(0, 0, 0, 0.3);
|
||||
@include mixins.box-shadow-post();
|
||||
}
|
||||
|
||||
.actor-infos {
|
||||
|
@ -7,6 +7,8 @@
|
||||
--bg2: #{colors.$dark-bg2};
|
||||
--bg3: #{colors.$dark-bg3};
|
||||
--bg4: #{colors.$dark-bg4};
|
||||
--bg5: #{colors.$dark-bg5};
|
||||
--bg6: #{colors.$dark-bg6};
|
||||
|
||||
--fg0: #{colors.$dark-fg0};
|
||||
--fg1: #{colors.$dark-fg1};
|
||||
@ -84,6 +86,10 @@
|
||||
--actor-accent-ok: #{colors.$dark-actor-accent-ok};
|
||||
--actor-accent2-ok: #{colors.$dark-actor-accent2-ok};
|
||||
}
|
||||
.loaded-files-list li {
|
||||
--actor-accent-ok: #{colors.$dark-actor-accent-ok};
|
||||
--actor-accent2-ok: #{colors.$dark-actor-accent2-ok};
|
||||
}
|
||||
|
||||
} @else {
|
||||
|
||||
@ -92,6 +98,8 @@
|
||||
--bg2: #{colors.$bg2};
|
||||
--bg3: #{colors.$bg3};
|
||||
--bg4: #{colors.$bg4};
|
||||
--bg5: #{colors.$bg5};
|
||||
--bg6: #{colors.$bg6};
|
||||
|
||||
--fg0: #{colors.$fg0};
|
||||
--fg1: #{colors.$fg1};
|
||||
@ -168,5 +176,9 @@
|
||||
--actor-accent-ok: #{colors.$actor-accent-ok};
|
||||
--actor-accent2-ok: #{colors.$actor-accent2-ok};
|
||||
}
|
||||
.loaded-files-list li {
|
||||
--actor-accent-ok: #{colors.$actor-accent-ok};
|
||||
--actor-accent2-ok: #{colors.$actor-accent2-ok};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,10 +6,12 @@ $hue: 30; // === 59.17 in oklch
|
||||
/*********************************/
|
||||
|
||||
$bg0: #fff; // post content
|
||||
$bg1: hsl($hue, 10%, 95%); // post
|
||||
$bg1: hsl($hue, 12%, 95%); // post
|
||||
$bg2: hsl($hue, 12%, 90%); // post attachment/meta/raw
|
||||
$bg3: hsl($hue, 15%, 87%); // posts header
|
||||
$bg4: hsl($hue, 17.5%, 84%); // filters/tags panels
|
||||
$bg5: hsl($hue, 25%, 95%); // settings
|
||||
$bg6: #fff; // settings > ul
|
||||
|
||||
$fg0: #000; // post content
|
||||
$fg1: hsl($hue, 10%, 20%); // body text; some SVG icons (panel close)
|
||||
@ -22,6 +24,8 @@ $dark-bg1: hsl($hue, 8%, 13%);
|
||||
$dark-bg2: hsl($hue, 10%, 10%);
|
||||
$dark-bg3: hsl($hue, 12%, 8%);
|
||||
$dark-bg4: hsl($hue, 12%, 8%);
|
||||
$dark-bg5: $dark-bg2;
|
||||
$dark-bg6: rgba(255, 255, 255, 0.07);
|
||||
$dark-fg0: hsl($hue, 5%, 95%);
|
||||
$dark-fg1: hsl($hue, 10%, 80%);
|
||||
$dark-fg2: hsl($hue, 15%, 50%);
|
||||
|
@ -1,5 +1,5 @@
|
||||
@use 'mixins';
|
||||
@use 'colors-mixins';
|
||||
@use "mixins";
|
||||
@use "colors-mixins";
|
||||
|
||||
/* mini-reset */
|
||||
*,
|
||||
@ -36,7 +36,7 @@ html {
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
color-scheme: dark;
|
||||
@include colors-mixins.theme-vars('dark');
|
||||
@include colors-mixins.theme-vars("dark");
|
||||
}
|
||||
}
|
||||
|
||||
@ -47,7 +47,7 @@ html.light {
|
||||
|
||||
html.dark {
|
||||
color-scheme: dark;
|
||||
@include colors-mixins.theme-vars('dark');
|
||||
@include colors-mixins.theme-vars("dark");
|
||||
}
|
||||
|
||||
/* global rules */
|
||||
@ -81,21 +81,33 @@ a,
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
button {
|
||||
position: relative;
|
||||
top: 0;
|
||||
input,
|
||||
button,
|
||||
label.btn,
|
||||
select {
|
||||
padding: 0.3em 0.5em;
|
||||
line-height: 24px;
|
||||
font-family: inherit;
|
||||
vertical-align: middle;
|
||||
background: var(--bg-button);
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 0.15em 0 0 rgba(0, 0, 0, 0.35);
|
||||
|
||||
@media (forced-colors: active) {
|
||||
border: 1px solid ButtonText;
|
||||
}
|
||||
}
|
||||
|
||||
button,
|
||||
label.btn,
|
||||
select {
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
button,
|
||||
label.btn {
|
||||
position: relative;
|
||||
top: 0;
|
||||
line-height: 24px;
|
||||
vertical-align: middle;
|
||||
background: var(--bg-button);
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 0.15em 0 0 rgba(0, 0, 0, 0.35);
|
||||
|
||||
&:not(:has(svg)) {
|
||||
padding: 0.7em 1em;
|
||||
@ -163,16 +175,11 @@ button {
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
padding: 0.3em 0.5em;
|
||||
input,
|
||||
select {
|
||||
color: inherit;
|
||||
border: none;
|
||||
background-color: var(--bg-input);
|
||||
|
||||
@media (forced-colors: active) {
|
||||
border: 1px solid ButtonText;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--bg-input-hover);
|
||||
}
|
||||
@ -182,14 +189,14 @@ input {
|
||||
}
|
||||
}
|
||||
|
||||
input[type='number'] {
|
||||
input[type="number"] {
|
||||
width: 6ch;
|
||||
text-align: center;
|
||||
-moz-appearance: textfield;
|
||||
appearance: textfield;
|
||||
}
|
||||
input[type='number']::-webkit-inner-spin-button,
|
||||
input[type='number']::-webkit-outer-spin-button {
|
||||
input[type="number"]::-webkit-inner-spin-button,
|
||||
input[type="number"]::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
@ -243,19 +250,28 @@ details {
|
||||
// focusable elements
|
||||
*:focus-visible,
|
||||
.tags-group button:focus-visible div,
|
||||
.toots-filter:has([type='checkbox']:focus-visible) {
|
||||
.toots-filter:has([type="checkbox"]:focus-visible) {
|
||||
text-decoration: none;
|
||||
outline: 2px solid var(--accent-light);
|
||||
outline-offset: 2px;
|
||||
border-radius: 2px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
&.mobile-menu-panel {
|
||||
outline-offset: -4px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.visually-hidden:not(:focus):not(:active) {
|
||||
@include mixins.visually-hidden();
|
||||
}
|
||||
|
||||
[inert] > * {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.nojs {
|
||||
margin: 5dvh 10dvw;
|
||||
padding: 2em;
|
||||
|
@ -11,11 +11,12 @@ html {
|
||||
z-index: 1;
|
||||
height: 100dvh;
|
||||
max-width: 100dvw;
|
||||
overflow: hidden;
|
||||
display: grid;
|
||||
grid-template-areas:
|
||||
"tools actor filters header tags"
|
||||
"tools actor filters toots tags";
|
||||
grid-template-columns: 150px min(25%, 600px) min(20%, 400px) 1fr min(20%, 400px);
|
||||
"actor filters header tags"
|
||||
"actor filters toots tags";
|
||||
grid-template-columns: min(25%, 600px) min(20%, 400px) 1fr min(20%, 400px);
|
||||
grid-template-rows: min-content 1fr;
|
||||
}
|
||||
|
||||
@ -34,9 +35,6 @@ html {
|
||||
.toots-tags {
|
||||
grid-area: tags;
|
||||
}
|
||||
.panel-tools {
|
||||
grid-area: tools;
|
||||
}
|
||||
.mobile-menu {
|
||||
display: none;
|
||||
}
|
||||
@ -57,7 +55,7 @@ html {
|
||||
.toggle-order {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.load-new {
|
||||
.open-tools {
|
||||
float: right;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
@ -116,6 +116,36 @@ $panel-width: 400px;
|
||||
bottom: 0;
|
||||
z-index: 3;
|
||||
background-color: var(--menu-backdrop);
|
||||
|
||||
.main-page.menu-open & {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.panel-close {
|
||||
position: absolute;
|
||||
right: 6px;
|
||||
top: 6px;
|
||||
z-index: 1;
|
||||
color: var(--panel-close);
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
.btn-icon {
|
||||
fill: currentColor;
|
||||
}
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:focus-visible {
|
||||
color: var(--panel-close-hover);
|
||||
}
|
||||
|
||||
@media (forced-colors: active) {
|
||||
color: buttonText;
|
||||
}
|
||||
|
||||
.actor & {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (width < 1200px) {
|
||||
@ -124,7 +154,7 @@ $panel-width: 400px;
|
||||
.toots-header {
|
||||
z-index: 2;
|
||||
box-shadow: 0 -10px 10px 10px rgba(0, 0, 0, 0.5);
|
||||
.load-new {
|
||||
.open-tools {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@ -147,6 +177,7 @@ $panel-width: 400px;
|
||||
.mobile-menu-panel {
|
||||
position: fixed;
|
||||
left: 0 - ($menu-width + $panel-width);
|
||||
right: auto;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 3;
|
||||
@ -163,31 +194,6 @@ $panel-width: 400px;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
}
|
||||
.panel-close {
|
||||
position: absolute;
|
||||
right: 6px;
|
||||
top: 6px;
|
||||
z-index: 1;
|
||||
color: var(--panel-close);
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
.btn-icon {
|
||||
fill: currentColor;
|
||||
}
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:focus-visible {
|
||||
color: var(--panel-close-hover);
|
||||
}
|
||||
|
||||
@media (forced-colors: active) {
|
||||
color: buttonText;
|
||||
}
|
||||
|
||||
.actor & {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-menu {
|
||||
display: block;
|
||||
@ -196,13 +202,10 @@ $panel-width: 400px;
|
||||
}
|
||||
|
||||
.main-page.menu-open {
|
||||
.panel-backdrop {
|
||||
display: block;
|
||||
}
|
||||
|
||||
&.menu-open-actor .actor,
|
||||
&.menu-open-filters .toots-filters,
|
||||
&.menu-open-tags .toots-tags {
|
||||
&.menu-open-tags .toots-tags,
|
||||
&.menu-open-tools .tools {
|
||||
left: $menu-width;
|
||||
box-shadow: -1rem 0 1rem 1rem rgba(0, 0, 0, 0.5);
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
@ -212,7 +215,8 @@ $panel-width: 400px;
|
||||
|
||||
&.menu-open-actor .menu-actor,
|
||||
&.menu-open-filters .menu-filters,
|
||||
&.menu-open-tags .menu-tags {
|
||||
&.menu-open-tags .menu-tags,
|
||||
&.menu-open-tools .menu-tools {
|
||||
color: var(--menu-fg-active);
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
@ -258,7 +262,8 @@ $panel-width: 400px;
|
||||
.main-page.menu-open {
|
||||
&.menu-open-actor .actor,
|
||||
&.menu-open-filters .toots-filters,
|
||||
&.menu-open-tags .toots-tags {
|
||||
&.menu-open-tags .toots-tags,
|
||||
&.menu-open-tools .tools {
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
@ -9,5 +9,8 @@
|
||||
}
|
||||
|
||||
@mixin box-shadow-inner() {
|
||||
box-shadow: 0 0.1rem 0.2rem -0.2rem rgba(0, 0, 0, 0.5) inset;
|
||||
box-shadow: 0 0.15rem 0.3rem -0.2rem rgba(0, 0, 0, 0.35) inset;
|
||||
}
|
||||
@mixin box-shadow-post() {
|
||||
box-shadow: 0 0.3rem 0.4rem -0.2rem rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ $display-wide: 340px;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
display: grid;
|
||||
padding: 1rem 1rem 0;
|
||||
padding: 0.5rem 1rem;
|
||||
grid-template-rows: min-content 1fr;
|
||||
|
||||
ul {
|
||||
@ -92,15 +92,14 @@ $display-wide: 340px;
|
||||
}
|
||||
|
||||
.tags-group-header {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
||||
@container (width >= #{$display-wide}) {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 0.25rem 1rem;
|
||||
}
|
||||
h3 {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
.count {
|
||||
color: var(--accent-dark);
|
||||
font-weight: normal;
|
||||
@ -121,7 +120,7 @@ $display-wide: 340px;
|
||||
|
||||
.tags-group-scroll {
|
||||
overflow: auto;
|
||||
padding-top: 0.5rem;
|
||||
margin-top: 0.5rem;
|
||||
padding-bottom: 2rem;
|
||||
}
|
||||
|
||||
|
@ -1,3 +1,199 @@
|
||||
.panel-tools {
|
||||
padding: 1rem;
|
||||
@use "mixins";
|
||||
|
||||
.tools-section {
|
||||
h3 {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
ul {
|
||||
list-style: none;
|
||||
font-size: 0.85em;
|
||||
margin: 1rem 0;
|
||||
padding: 0.5rem 1rem;
|
||||
background-color: var(--bg6);
|
||||
border-radius: 0.5rem;
|
||||
word-break: break-all;
|
||||
}
|
||||
li {
|
||||
list-style: none;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
}
|
||||
|
||||
.panel-tools {
|
||||
position: absolute;
|
||||
left: auto;
|
||||
right: calc(0px - min(25%, 600px));
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 4;
|
||||
|
||||
width: min(25%, 600px);
|
||||
height: 100dvh;
|
||||
overflow-y: scroll;
|
||||
padding: 0;
|
||||
|
||||
box-shadow: 0.75rem 0 1rem 0.75rem rgba(0, 0, 0, 0.35);
|
||||
background-color: var(--bg5);
|
||||
backdrop-filter: blur(20px);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
transition: right 0.2s ease-out;
|
||||
transition-property: right, box-shadow;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
.menu-open-tools & {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.panel-close {
|
||||
display: block !important;
|
||||
}
|
||||
.tools-section {
|
||||
padding: 1rem 1rem 2rem;
|
||||
&.app-log {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.app-settings {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
h3 {
|
||||
margin: 0;
|
||||
flex: 0 0 100%;
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
|
||||
.switch-lang {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
line-height: 24px;
|
||||
label {
|
||||
display: block;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 0.3rem 0.5rem;
|
||||
svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
fill: var(--button-svg);
|
||||
@media (forced-colors: active) {
|
||||
fill: ButtonText;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.switch-theme {
|
||||
&.theme-dark {
|
||||
.btn-label.dark {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
&.theme-light {
|
||||
.btn-label.light {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.loaded-files-list {
|
||||
@include mixins.box-shadow-inner();
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
}
|
||||
li + li {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.name {
|
||||
flex: 0 0 100%;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
@media (forced-colors: none) {
|
||||
.multiple-files & li {
|
||||
position: relative;
|
||||
padding-left: 1.25rem;
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 1;
|
||||
width: 0.25rem;
|
||||
border-radius: 0.25rem;
|
||||
background-color: var(--actor-accent-ok, transparent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.manage-files-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
|
||||
.file-loader {
|
||||
@include mixins.visually-hidden();
|
||||
}
|
||||
|
||||
.tip {
|
||||
flex: 0 0 100%;
|
||||
font-size: 0.8em;
|
||||
text-align: center;
|
||||
font-style: italic;
|
||||
opacity: 0.85;
|
||||
}
|
||||
}
|
||||
|
||||
.app-log {
|
||||
ul {
|
||||
height: 20dvh;
|
||||
overflow-y: auto;
|
||||
@include mixins.box-shadow-inner();
|
||||
}
|
||||
.msg {
|
||||
b {
|
||||
font-weight: normal;
|
||||
color: var(--accent);
|
||||
}
|
||||
}
|
||||
.time {
|
||||
opacity: 0.65;
|
||||
}
|
||||
}
|
||||
|
||||
.about {
|
||||
font-size: 0.85em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.welcome {
|
||||
.tools {
|
||||
margin-top: auto;
|
||||
|
||||
h3,
|
||||
.app-log {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.manage-files {
|
||||
display: none;
|
||||
}
|
||||
.about {
|
||||
margin-top: 4rem;
|
||||
}
|
||||
}
|
||||
|
@ -62,7 +62,7 @@ $meta-visible: 100ch;
|
||||
overflow-wrap: break-word;
|
||||
background-color: var(--bg0);
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 0.3rem 0.4rem -0.2rem rgba(0, 0, 0, 0.3);
|
||||
@include mixins.box-shadow-post();
|
||||
|
||||
@media (forced-colors: active) {
|
||||
border: 1px solid CanvasText;
|
||||
@ -113,7 +113,6 @@ $meta-visible: 100ch;
|
||||
z-index: 1;
|
||||
width: 0.25rem;
|
||||
border-radius: 0.25rem;
|
||||
background-color: red;
|
||||
background-color: var(--actor-accent-ok, transparent);
|
||||
}
|
||||
}
|
||||
@ -193,7 +192,7 @@ $meta-visible: 100ch;
|
||||
object-fit: cover;
|
||||
font-size: 0.6em;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 0.3rem 0.4rem -0.2rem rgba(0, 0, 0, 0.3);
|
||||
@include mixins.box-shadow-post();
|
||||
mix-blend-mode: multiply;
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
transition: filter 0.2s ease-out;
|
||||
@ -253,7 +252,7 @@ $meta-visible: 100ch;
|
||||
max-width: 100%;
|
||||
max-height: 75dvh;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 0.3rem 0.4rem -0.2rem rgba(0, 0, 0, 0.3);
|
||||
@include mixins.box-shadow-post();
|
||||
}
|
||||
.att-description {
|
||||
padding: 1rem;
|
||||
|
@ -18,10 +18,6 @@
|
||||
line-height: 1.4;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
.load-new {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.intro {
|
||||
|
@ -22,6 +22,9 @@ import SvgSprites from '../components/SvgSprites.astro';
|
||||
<SvgSprites />
|
||||
|
||||
<script src="/js/strings.js" is:inline></script>
|
||||
<script src="/js/main.js" is:inline></script>
|
||||
<script src="/js/stores.js" is:inline></script>
|
||||
<script src="/js/utils.js" is:inline></script>
|
||||
<script src="/js/libs.js" is:inline></script>
|
||||
<script src="/js/init.js" is:inline></script>
|
||||
</body>
|
||||
</html>
|
||||
|
1
dist/_astro/index.33CKCK6j.css
vendored
Normal file
1
dist/_astro/index.33CKCK6j.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/_astro/index.uwcihdcd.css
vendored
1
dist/_astro/index.uwcihdcd.css
vendored
File diff suppressed because one or more lines are too long
18
dist/index.html
vendored
18
dist/index.html
vendored
File diff suppressed because one or more lines are too long
30
dist/js/init.js
vendored
Normal file
30
dist/js/init.js
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
drag.init("app");
|
||||
|
||||
document.addEventListener("alpine:init", () => {
|
||||
// create and init stores
|
||||
Alpine.store("files", filesStore);
|
||||
Alpine.store("lightbox", lightboxStore);
|
||||
Alpine.store("ui", uiStore);
|
||||
Alpine.store("userPrefs", userPrefsStore);
|
||||
|
||||
const salutations = [
|
||||
"Hi!",
|
||||
"Hiya!",
|
||||
"Hello there!",
|
||||
"Good day!",
|
||||
"Hullo!",
|
||||
"Buongiorno!",
|
||||
"Guten Tag!",
|
||||
"Bonjour!",
|
||||
"Oh hey!",
|
||||
];
|
||||
Alpine.store("ui").logMsg(`MARL loaded. ${salutations[Math.floor(Math.random() * salutations.length)]} 😊`);
|
||||
|
||||
resetStores();
|
||||
});
|
||||
|
||||
document.addEventListener("alpine-i18n:ready", function () {
|
||||
AlpineI18n.create("en", appStrings);
|
||||
AlpineI18n.fallbackLocale = "en";
|
||||
setLang();
|
||||
});
|
36
dist/js/libs.js
vendored
Normal file
36
dist/js/libs.js
vendored
Normal file
@ -0,0 +1,36 @@
|
||||
const isFileProtocol = window.location.protocol === "file:";
|
||||
|
||||
const scripts = [
|
||||
{
|
||||
src: "js/jszip.min.js",
|
||||
integrity: "sha512-XMVd28F1oH/O71fzwBnV7HucLxVwtxf26XV8P4wPk26EDxuGZ91N8bsOttmnomcCD3CS5ZMRL50H0GgOHvegtg==",
|
||||
crossorigin: "anonymous",
|
||||
defer: false,
|
||||
},
|
||||
// Note: Alpine plug-ins must be inserted BEFORE alpinejs
|
||||
{
|
||||
src: "js/alpinejs-i18n.min.js",
|
||||
integrity: "sha256-o204NcFyHPFzboSC51fufMqFe2KJdQfSCl8AlvSZO/E=",
|
||||
crossorigin: "anonymous",
|
||||
defer: true,
|
||||
},
|
||||
{
|
||||
src: "js/alpinejs.min.js",
|
||||
integrity: "sha512-FUaEyIgi9bspXaH6hUadCwBLxKwdH7CW24riiOqA5p8hTNR/RCLv9UpAILKwqs2AN5WtKB52CqbiePBei3qjKg==",
|
||||
crossorigin: "anonymous",
|
||||
defer: true,
|
||||
},
|
||||
];
|
||||
|
||||
scripts.forEach(({ src, integrity, crossorigin, defer }) => {
|
||||
const script = document.createElement("script");
|
||||
script.src = src;
|
||||
if (!isFileProtocol) {
|
||||
script.integrity = integrity;
|
||||
script.crossOrigin = crossorigin;
|
||||
}
|
||||
if (defer) {
|
||||
script.defer = true;
|
||||
}
|
||||
document.head.appendChild(script);
|
||||
});
|
1507
dist/js/main.js
vendored
1507
dist/js/main.js
vendored
File diff suppressed because it is too large
Load Diff
888
dist/js/stores.js
vendored
Normal file
888
dist/js/stores.js
vendored
Normal file
@ -0,0 +1,888 @@
|
||||
// stores definitions
|
||||
|
||||
const userPrefsStore = {
|
||||
prefix: "marl_",
|
||||
|
||||
save(pref, value) {
|
||||
const msg = `Saving user preference <b>(${pref}: ${value})</b>`;
|
||||
Alpine.store("ui").logMsg(msg, "info");
|
||||
localStorage.setItem(this.prefix + pref, value);
|
||||
},
|
||||
load(pref) {
|
||||
const value = localStorage.getItem(this.prefix + pref);
|
||||
if (value !== null) {
|
||||
this.set(pref, value);
|
||||
} else if (pref === "lang") {
|
||||
this.set(pref, value);
|
||||
}
|
||||
},
|
||||
set(pref, value) {
|
||||
switch (pref) {
|
||||
case "sortAsc":
|
||||
value = +value === 1 ? true : false;
|
||||
if (value !== Alpine.store("files").sortAsc) {
|
||||
Alpine.store("files").sortAsc = value;
|
||||
}
|
||||
break;
|
||||
|
||||
case "pageSize":
|
||||
value = +value;
|
||||
if (typeof value == "number" && !isNaN(value) && value > 0 && value !== Alpine.store("files").pageSize) {
|
||||
Alpine.store("files").pageSize = value;
|
||||
}
|
||||
break;
|
||||
|
||||
case "lang":
|
||||
if (!value) {
|
||||
value = detectLangFromBrowser();
|
||||
if (value) {
|
||||
this.save("lang", value);
|
||||
}
|
||||
}
|
||||
if (!value || !Alpine.store("ui").appLangs[value]) {
|
||||
if (value) {
|
||||
const msg = `<b>Unrecognized language</b> in user preferences: ${value}`;
|
||||
console.warn(msg);
|
||||
Alpine.store("ui").logMsg(msg, "warn");
|
||||
}
|
||||
value = "en";
|
||||
this.save("lang", value);
|
||||
}
|
||||
Alpine.store("ui").lang = value;
|
||||
break;
|
||||
|
||||
case "theme":
|
||||
if (!(value === "dark" || value === "light")) {
|
||||
value = "light";
|
||||
this.save("theme", value);
|
||||
}
|
||||
Alpine.store("ui").theme = value;
|
||||
setTheme(value);
|
||||
break;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const filesStore = {
|
||||
resetState() {
|
||||
this.sources = [];
|
||||
this.toots = [];
|
||||
this.toc = [];
|
||||
this.duplicates = false;
|
||||
|
||||
this.sortAsc = true; // -> userPrefs
|
||||
this.pageSize = 10; // -> userPrefs
|
||||
this.currentPage = 1;
|
||||
|
||||
this.loading = false;
|
||||
this.someFilesLoaded = false;
|
||||
|
||||
this.languages = {};
|
||||
this.boostsAuthors = [];
|
||||
|
||||
this.filters = {};
|
||||
this.filtersDefault = {
|
||||
fullText: "",
|
||||
hashtagText: "",
|
||||
mentionText: "",
|
||||
externalLink: "",
|
||||
summary: "",
|
||||
isEdited: false,
|
||||
isDuplicate: false,
|
||||
noStartingAt: false,
|
||||
hasExternalLink: false,
|
||||
hasHashtags: false,
|
||||
hasMentions: false,
|
||||
hasSummary: false,
|
||||
isSensitive: false,
|
||||
visibilityPublic: true,
|
||||
visibilityUnlisted: true,
|
||||
visibilityFollowers: true,
|
||||
visibilityMentioned: true,
|
||||
typeOriginal: true,
|
||||
typeBoost: true,
|
||||
attachmentAny: false,
|
||||
attachmentImage: false,
|
||||
attachmentVideo: false,
|
||||
attachmentSound: false,
|
||||
attachmentNoAltText: false,
|
||||
attachmentWithAltText: false,
|
||||
|
||||
// automatically generated (see loadJsonFile()):
|
||||
// lang_en: true,
|
||||
// lang_fr: true,
|
||||
// lang_de: true,
|
||||
// etc
|
||||
// actor_0: true,
|
||||
// actor_1: true,
|
||||
// actor_2: true,
|
||||
// etc
|
||||
};
|
||||
this.filtersActive = false;
|
||||
|
||||
this.tagsFilters = {
|
||||
hashtags: "",
|
||||
mentions: "",
|
||||
boostsAuthors: "",
|
||||
};
|
||||
|
||||
Alpine.store("userPrefs").load("sortAsc");
|
||||
Alpine.store("userPrefs").load("pageSize");
|
||||
},
|
||||
|
||||
setFilter() {
|
||||
this.checkPagingValue();
|
||||
scrollTootsToTop();
|
||||
pagingUpdated();
|
||||
if (JSON.stringify(this.filters) === JSON.stringify(this.filtersDefault)) {
|
||||
this.filtersActive = false;
|
||||
} else {
|
||||
this.filtersActive = true;
|
||||
}
|
||||
|
||||
const self = this;
|
||||
setTimeout(() => {
|
||||
self.checkPagingValue();
|
||||
}, 50);
|
||||
},
|
||||
filterByTag(filter, value, id) {
|
||||
if (value) {
|
||||
if (value === this.filters[filter]) {
|
||||
this.filters[filter] = "";
|
||||
} else {
|
||||
this.filters[filter] = value;
|
||||
}
|
||||
}
|
||||
|
||||
// "boosted users" group
|
||||
// in this case let's also (un)check the 'boost type' filters
|
||||
if (filter == "fullText") {
|
||||
if (this.filters[filter] === "") {
|
||||
this.filters.typeBoost = true;
|
||||
this.filters.typeOriginal = true;
|
||||
} else {
|
||||
this.filters.typeBoost = true;
|
||||
this.filters.typeOriginal = false;
|
||||
}
|
||||
}
|
||||
|
||||
this.setFilter();
|
||||
|
||||
// keyboard focus may be lost when tags list changes
|
||||
setTimeout(() => {
|
||||
document.getElementById(id).focus();
|
||||
}, 100);
|
||||
},
|
||||
resetFilters(userAction) {
|
||||
this.filters = JSON.parse(JSON.stringify(this.filtersDefault));
|
||||
if (userAction) {
|
||||
this.currentPage = 1;
|
||||
this.filtersActive = false;
|
||||
scrollTootsToTop();
|
||||
pagingUpdated();
|
||||
}
|
||||
},
|
||||
|
||||
get filteredToots() {
|
||||
const f = this.filters;
|
||||
const fa = this.filtersActive;
|
||||
return this.toots.filter((t) => {
|
||||
if (!fa) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (f.fullText) {
|
||||
let show = false;
|
||||
if (t._marl.textContent) {
|
||||
const filterValue = f.fullText.toLowerCase();
|
||||
|
||||
if (filterValue && t._marl.textContent && t._marl.textContent.indexOf(filterValue) >= 0) {
|
||||
show = true;
|
||||
}
|
||||
}
|
||||
if (!show) {
|
||||
return show;
|
||||
}
|
||||
}
|
||||
|
||||
if (f.hashtagText) {
|
||||
if (typeof t.object === "object" && t.object !== null && t.object.tag) {
|
||||
const filterValue = f.hashtagText.toLowerCase();
|
||||
if (
|
||||
!t.object.tag.some((t) => {
|
||||
return t.type === "Hashtag" && t.name.toLowerCase().indexOf(filterValue) > -1;
|
||||
})
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (f.mentionText) {
|
||||
if (typeof t.object === "object" && t.object !== null && t.object.tag) {
|
||||
const filterValue = f.mentionText.toLowerCase();
|
||||
if (
|
||||
!t.object.tag.some((t) => {
|
||||
return t.type === "Mention" && t.name.toLowerCase().indexOf(filterValue) > -1;
|
||||
})
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (f.summary) {
|
||||
if (t._marl.summary) {
|
||||
const filterValue = f.summary.toLowerCase();
|
||||
if (t._marl.summary.indexOf(filterValue) === -1) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (f.isEdited) {
|
||||
if (!(typeof t.object === "object" && t.object !== null && t.object.updated)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (f.isDuplicate) {
|
||||
if (!t._marl.duplicate) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (f.noStartingAt) {
|
||||
if (!t._marl.textContent || t._marl.textContent.indexOf("@") === 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (f.hasExternalLink) {
|
||||
if (!t._marl.externalLinks || !t._marl.externalLinks.length) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (f.hasHashtags) {
|
||||
if (typeof t.object === "object" && t.object !== null && t.object.tag) {
|
||||
if (
|
||||
!t.object.tag.some((t) => {
|
||||
return t.type === "Hashtag";
|
||||
})
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (f.hasMentions) {
|
||||
if (typeof t.object === "object" && t.object !== null && t.object.tag) {
|
||||
if (
|
||||
!t.object.tag.some((t) => {
|
||||
return t.type === "Mention";
|
||||
})
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (f.hasSummary) {
|
||||
if (typeof t.object === "object" && t.object !== null) {
|
||||
if (!t.object.summary) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (f.isSensitive) {
|
||||
if (typeof t.object === "object" && t.object !== null) {
|
||||
if (!t.object.sensitive) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (f.externalLink) {
|
||||
let show = false;
|
||||
if (t._marl.externalLinks && t._marl.externalLinks.length) {
|
||||
const filterValue = f.externalLink.toLowerCase();
|
||||
show = t._marl.externalLinks.some((link) => {
|
||||
return link.href.indexOf(filterValue) > -1 || link.text.indexOf(filterValue) > -1;
|
||||
});
|
||||
}
|
||||
if (!show) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!f.visibilityPublic && t._marl.visibility[0] === "public") {
|
||||
return false;
|
||||
}
|
||||
if (!f.visibilityUnlisted && t._marl.visibility[0] === "unlisted") {
|
||||
return false;
|
||||
}
|
||||
if (!f.visibilityFollowers && t._marl.visibility[0] === "followers") {
|
||||
return false;
|
||||
}
|
||||
if (!f.visibilityMentioned && t._marl.visibility[0] === "mentioned") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!f.typeOriginal && t.type === "Create") {
|
||||
return false;
|
||||
}
|
||||
if (!f.typeBoost && t.type === "Announce") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (f.attachmentAny) {
|
||||
if (!t._marl.hasAttachments) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (f.attachmentImage) {
|
||||
if (t._marl.hasAttachments) {
|
||||
if (
|
||||
!t.object.attachment.some((att) => {
|
||||
return attachmentIsImage(att);
|
||||
})
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (f.attachmentVideo) {
|
||||
if (t._marl.hasAttachments) {
|
||||
if (
|
||||
!t.object.attachment.some((att) => {
|
||||
return attachmentIsVideo(att);
|
||||
})
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (f.attachmentSound) {
|
||||
if (t._marl.hasAttachments) {
|
||||
if (
|
||||
!t.object.attachment.some((att) => {
|
||||
return attachmentIsSound(att);
|
||||
})
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (f.attachmentNoAltText) {
|
||||
if (t._marl.hasAttachments) {
|
||||
if (
|
||||
!t.object.attachment.some((att) => {
|
||||
return att.name === null;
|
||||
})
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (f.attachmentWithAltText) {
|
||||
if (t._marl.hasAttachments) {
|
||||
if (
|
||||
!t.object.attachment.some((att) => {
|
||||
return att.name;
|
||||
})
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
for (const lang in this.languages) {
|
||||
if (f.hasOwnProperty("lang_" + lang) && f["lang_" + lang] === false) {
|
||||
if (t._marl.langs.includes(lang) || t._marl.langs.length === 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const source of this.sources) {
|
||||
const id = source.id;
|
||||
if (f.hasOwnProperty("actor_" + id) && f["actor_" + id] === false) {
|
||||
if (t._marl.source === id) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
},
|
||||
|
||||
get listHashtags() {
|
||||
return this.listTags("Hashtag");
|
||||
},
|
||||
get listMentions() {
|
||||
return this.listTags("Mention");
|
||||
},
|
||||
listTags(type) {
|
||||
let filterSource = "";
|
||||
switch (type) {
|
||||
case "Mention":
|
||||
filterSource = "mentions";
|
||||
break;
|
||||
case "Hashtag":
|
||||
filterSource = "hashtags";
|
||||
break;
|
||||
}
|
||||
let h = this.filteredToots.reduce((accu, toot) => {
|
||||
if (tootHasTags(toot)) {
|
||||
for (const key in toot.object.tag) {
|
||||
const tag = toot.object.tag[key];
|
||||
if (
|
||||
tag.type &&
|
||||
tag.type === type &&
|
||||
tag.name &&
|
||||
tag.name.toLowerCase().indexOf(this.tagsFilters[filterSource].toLowerCase()) >= 0
|
||||
) {
|
||||
if (
|
||||
accu.some((item) => {
|
||||
return item.name === tag.name;
|
||||
})
|
||||
) {
|
||||
accu.map((item) => {
|
||||
if (item.name === tag.name) {
|
||||
item.nb++;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
accu.push({
|
||||
name: tag.name,
|
||||
href: tag.href,
|
||||
nb: 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return accu;
|
||||
}, []);
|
||||
|
||||
h.sort((a, b) => {
|
||||
if (a.nb === b.nb) {
|
||||
return a.name.localeCompare(b.name);
|
||||
} else {
|
||||
return b.nb - a.nb;
|
||||
}
|
||||
});
|
||||
|
||||
return h;
|
||||
},
|
||||
get listBoostsAuthors() {
|
||||
let r = this.boostsAuthors.reduce((accu, item) => {
|
||||
if (item.name.toLowerCase().indexOf(this.tagsFilters.boostsAuthors.toLowerCase()) >= 0) {
|
||||
accu.push(item);
|
||||
}
|
||||
return accu;
|
||||
}, []);
|
||||
r.sort((a, b) => {
|
||||
if (a.nb === b.nb) {
|
||||
let aHasNoName = a.name.indexOf("? ") === 0;
|
||||
let bHasNoName = b.name.indexOf("? ") === 0;
|
||||
if (aHasNoName && bHasNoName) {
|
||||
return a.name.localeCompare(b.name);
|
||||
} else if (aHasNoName) {
|
||||
return 1;
|
||||
} else if (bHasNoName) {
|
||||
return -1;
|
||||
} else {
|
||||
return a.name.localeCompare(b.name);
|
||||
}
|
||||
} else {
|
||||
if (a.nb === b.nb) {
|
||||
return a.name.localeCompare(b.name);
|
||||
} else {
|
||||
return b.nb - a.nb;
|
||||
}
|
||||
}
|
||||
});
|
||||
return r;
|
||||
},
|
||||
|
||||
get sortedLanguages() {
|
||||
let langs = [];
|
||||
for (const lang in this.languages) {
|
||||
langs.push([lang, this.languages[lang]]);
|
||||
}
|
||||
langs.sort((a, b) => {
|
||||
if (a[0] === "undefined") {
|
||||
return 1;
|
||||
}
|
||||
if (b[0] === "undefined") {
|
||||
return -1;
|
||||
}
|
||||
if (a[1] === b[1]) {
|
||||
return a[0].localeCompare(b[0]);
|
||||
}
|
||||
return b[1] - a[1];
|
||||
});
|
||||
return langs;
|
||||
},
|
||||
|
||||
get appReady() {
|
||||
if (this.sources.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let r = true;
|
||||
for (let i = 0; i < this.sources.length; i++) {
|
||||
const source = this.sources[i];
|
||||
if (
|
||||
!source.loaded.actor ||
|
||||
!source.loaded.avatar ||
|
||||
!source.loaded.header ||
|
||||
!source.loaded.outbox ||
|
||||
!source.loaded.likes ||
|
||||
!source.loaded.bookmarks
|
||||
) {
|
||||
r = false;
|
||||
}
|
||||
}
|
||||
return r;
|
||||
},
|
||||
|
||||
get totalPages() {
|
||||
return Math.ceil(this.filteredToots.length / this.pageSize);
|
||||
},
|
||||
get pagedToots() {
|
||||
if (this.filteredToots) {
|
||||
return this.filteredToots.filter((_, index) => {
|
||||
let start = (this.currentPage - 1) * this.pageSize;
|
||||
let end = this.currentPage * this.pageSize;
|
||||
if (index >= start && index < end) return true;
|
||||
});
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
sortToots() {
|
||||
this.toots.sort((a, b) => {
|
||||
if (this.sortAsc) {
|
||||
return a.published.localeCompare(b.published);
|
||||
} else {
|
||||
return b.published.localeCompare(a.published);
|
||||
}
|
||||
});
|
||||
},
|
||||
toggleTootsOrder() {
|
||||
this.sortAsc = !this.sortAsc;
|
||||
Alpine.store("userPrefs").save("sortAsc", this.sortAsc ? 1 : 0);
|
||||
this.sortToots();
|
||||
scrollTootsToTop();
|
||||
pagingUpdated();
|
||||
},
|
||||
|
||||
setPostsPerPage() {
|
||||
this.checkPagingValue();
|
||||
Alpine.store("userPrefs").save("pageSize", this.pageSize);
|
||||
},
|
||||
checkPagingValue() {
|
||||
if (this.currentPage < 1) {
|
||||
this.currentPage = 1;
|
||||
} else if (this.currentPage > this.totalPages) {
|
||||
this.currentPage = this.totalPages;
|
||||
}
|
||||
},
|
||||
nextPage(setFocusTo) {
|
||||
if (this.currentPage * this.pageSize < this.filteredToots.length) {
|
||||
this.currentPage++;
|
||||
scrollTootsToTop(setFocusTo);
|
||||
pagingUpdated();
|
||||
}
|
||||
},
|
||||
prevPage(setFocusTo) {
|
||||
if (this.currentPage > 1) {
|
||||
this.currentPage--;
|
||||
scrollTootsToTop(setFocusTo);
|
||||
pagingUpdated();
|
||||
}
|
||||
},
|
||||
firstPage(setFocusTo) {
|
||||
this.currentPage = 1;
|
||||
scrollTootsToTop(setFocusTo);
|
||||
pagingUpdated();
|
||||
},
|
||||
lastPage(setFocusTo) {
|
||||
this.currentPage = this.totalPages;
|
||||
scrollTootsToTop(setFocusTo);
|
||||
pagingUpdated();
|
||||
},
|
||||
};
|
||||
|
||||
const lightboxStore = {
|
||||
resetState() {
|
||||
this.show = false;
|
||||
this.data = [];
|
||||
this.source = 0;
|
||||
this.index = 0;
|
||||
this.origin = "";
|
||||
},
|
||||
|
||||
open(toot, index, origin) {
|
||||
this.data = toot.object.attachment;
|
||||
this.source = toot._marl.source;
|
||||
this.show = true;
|
||||
this.index = index;
|
||||
this.origin = origin;
|
||||
document.getElementById("main-section-inner").setAttribute("inert", true);
|
||||
setTimeout(() => {
|
||||
document.getElementById("lightbox").focus();
|
||||
}, 50);
|
||||
},
|
||||
openProfileImg(name, origin, source) {
|
||||
const data = {
|
||||
object: {
|
||||
attachment: [
|
||||
{
|
||||
name: name,
|
||||
url: name,
|
||||
mediaType: Alpine.store("files").sources[source][name].type,
|
||||
},
|
||||
],
|
||||
},
|
||||
_marl: {
|
||||
source: source,
|
||||
},
|
||||
};
|
||||
this.open(data, 0, origin);
|
||||
},
|
||||
close() {
|
||||
const origin = this.origin;
|
||||
this.data = [];
|
||||
this.index = 0;
|
||||
this.show = false;
|
||||
this.origin = "";
|
||||
document.getElementById("main-section-inner").removeAttribute("inert");
|
||||
document.getElementById(origin).focus();
|
||||
},
|
||||
showNext() {
|
||||
this.index++;
|
||||
if (this.index >= this.data.length) {
|
||||
this.index = 0;
|
||||
}
|
||||
if (!attachmentIsImage(this.data[this.index])) {
|
||||
this.showNext();
|
||||
}
|
||||
},
|
||||
showPrev() {
|
||||
this.index--;
|
||||
if (this.index < 0) {
|
||||
this.index = this.data.length - 1;
|
||||
}
|
||||
if (!attachmentIsImage(this.data[this.index])) {
|
||||
this.showPrev();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const uiStore = {
|
||||
log: [],
|
||||
resetState() {
|
||||
this.pagingOptionsVisible = false;
|
||||
this.openMenu = "";
|
||||
this.actorPanel = 0;
|
||||
this.menuIsActive = false;
|
||||
this.lang = "en";
|
||||
this.appLangs = appLangs ?? { en: "English" };
|
||||
this.theme = "light";
|
||||
this.log = this.log ?? [];
|
||||
|
||||
Alpine.store("userPrefs").load("lang");
|
||||
Alpine.store("userPrefs").load("theme");
|
||||
},
|
||||
|
||||
logMsg(msg, type) {
|
||||
type = type ?? "info";
|
||||
const dateOptions = {
|
||||
hour12: false,
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
};
|
||||
const time = new Date().toLocaleTimeString(Alpine.store("ui").lang, dateOptions);
|
||||
let m = {
|
||||
msg: msg,
|
||||
type: type,
|
||||
time: time,
|
||||
};
|
||||
this.log.unshift(m);
|
||||
},
|
||||
|
||||
toggleTheme() {
|
||||
this.theme = this.theme === "light" ? "dark" : "light";
|
||||
Alpine.store("userPrefs").save("theme", this.theme);
|
||||
setTheme(this.theme);
|
||||
},
|
||||
|
||||
togglePagingOptions() {
|
||||
this.pagingOptionsVisible = !this.pagingOptionsVisible;
|
||||
|
||||
if (this.pagingOptionsVisible) {
|
||||
setTimeout(() => {
|
||||
document.getElementById("paging-options").focus();
|
||||
}, 100);
|
||||
}
|
||||
},
|
||||
get pagingOptionsClass() {
|
||||
return this.pagingOptionsVisible ? "open" : "";
|
||||
},
|
||||
|
||||
openActorPanel(id) {
|
||||
this.actorPanel = id;
|
||||
},
|
||||
switchActorPanel(dir) {
|
||||
let id = this.actorPanel;
|
||||
if (dir === "up") {
|
||||
id++;
|
||||
if (id >= Alpine.store("files").sources.length) {
|
||||
id = 0;
|
||||
}
|
||||
} else {
|
||||
id--;
|
||||
if (id < 0) {
|
||||
id = Alpine.store("files").sources.length - 1;
|
||||
}
|
||||
}
|
||||
this.actorPanel = id;
|
||||
document.getElementById("actortab-" + id).focus();
|
||||
},
|
||||
|
||||
menuClose() {
|
||||
const name = this.openMenu;
|
||||
this.openMenu = "";
|
||||
this.setInert();
|
||||
|
||||
// bring focus back to where it was before the panel was opened
|
||||
document.querySelector("#main-section-inner .mobile-menu .menu-" + name).focus();
|
||||
},
|
||||
menuOpen(name) {
|
||||
this.openMenu = name;
|
||||
this.resetPanels();
|
||||
this.setInert();
|
||||
|
||||
setTimeout(() => {
|
||||
document.getElementById("panel-" + name).focus();
|
||||
}, 100);
|
||||
},
|
||||
menuToggle(name) {
|
||||
switch (name) {
|
||||
case "actor":
|
||||
case "filters":
|
||||
case "tags":
|
||||
case "tools":
|
||||
if (this.openMenu === name) {
|
||||
this.menuClose();
|
||||
} else {
|
||||
this.menuOpen(name);
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
resetPanels() {
|
||||
const name = this.openMenu;
|
||||
document.querySelectorAll(`#panel-${name} details[open]`).forEach((e) => {
|
||||
e.removeAttribute("open");
|
||||
});
|
||||
setTimeout(() => {
|
||||
document.getElementById("panel-" + name).scrollTop = 0;
|
||||
}, 250);
|
||||
},
|
||||
checkMenuState() {
|
||||
const menu = document.getElementById("mobile-menu");
|
||||
if (window.getComputedStyle(menu, null).display === "none") {
|
||||
this.menuIsActive = false;
|
||||
} else {
|
||||
this.menuIsActive = true;
|
||||
}
|
||||
|
||||
this.setInert();
|
||||
},
|
||||
|
||||
setInertMain() {
|
||||
document
|
||||
.querySelectorAll("#main-section-inner > *:not(.mobile-menu, .panel-backdrop, #panel-" + this.openMenu)
|
||||
.forEach((e) => {
|
||||
e.setAttribute("inert", true);
|
||||
});
|
||||
},
|
||||
setInertPanels() {
|
||||
document.querySelectorAll("#panel-actor, #panel-filters, #panel-tags, #panel-tools").forEach((e) => {
|
||||
e.setAttribute("inert", true);
|
||||
});
|
||||
},
|
||||
setInertTools() {
|
||||
document.querySelectorAll("#panel-tools").forEach((e) => {
|
||||
e.setAttribute("inert", true);
|
||||
});
|
||||
},
|
||||
setInert() {
|
||||
// set the 'inert' state on the side panels or the main part of the app
|
||||
// depending on whether they are hidden or not, AND whether the mobile
|
||||
// menu is active
|
||||
|
||||
document.querySelectorAll("#main-section-inner > *").forEach((e) => {
|
||||
e.removeAttribute("inert");
|
||||
});
|
||||
|
||||
if (this.menuIsActive) {
|
||||
if (this.openMenu) {
|
||||
this.setInertMain();
|
||||
} else {
|
||||
this.setInertPanels();
|
||||
}
|
||||
} else {
|
||||
if (this.openMenu === "tools") {
|
||||
this.setInertMain();
|
||||
} else {
|
||||
this.setInertTools();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
get appClasses() {
|
||||
let classes = [];
|
||||
if (this.openMenu) {
|
||||
classes.push("menu-open menu-open-" + this.openMenu);
|
||||
} else {
|
||||
classes.push("menu-closed");
|
||||
}
|
||||
return classes;
|
||||
},
|
||||
};
|
208
dist/js/strings.js
vendored
208
dist/js/strings.js
vendored
@ -1,7 +1,7 @@
|
||||
const appLangs = [
|
||||
["en", "English"],
|
||||
["fr", "Français"],
|
||||
];
|
||||
const appLangs = {
|
||||
en: "English",
|
||||
fr: "Français",
|
||||
};
|
||||
|
||||
const appStrings = {
|
||||
en: {
|
||||
@ -16,7 +16,6 @@ const appStrings = {
|
||||
p3: `<strong>Start by opening your archive file with MARL.</strong><br />
|
||||
You can drag and drop it anywhere on this page, or
|
||||
{labelStart}click here to select it{labelEnd}.`,
|
||||
projectPage: `Project page (github)`,
|
||||
},
|
||||
misc: {
|
||||
loading: "Loading",
|
||||
@ -27,8 +26,7 @@ const appStrings = {
|
||||
filters: "Filters",
|
||||
filtersActive: "some filters are active",
|
||||
tags: "Tags",
|
||||
newFile: "New File",
|
||||
newFileConfirm: "Discard current data and load a new archive file?",
|
||||
tools: "Tools",
|
||||
},
|
||||
lightbox: {
|
||||
next: "Next image",
|
||||
@ -50,8 +48,8 @@ const appStrings = {
|
||||
cache. Posts that are not in your instance cache any more are not included in your
|
||||
archive. This affects boosts, likes, and bookmarks.`,
|
||||
rawData: "Raw data {fileName}",
|
||||
favorites: "Favorites",
|
||||
favoritesEmpty: "no favorites",
|
||||
likes: "Favorites",
|
||||
likesEmpty: "no favorites",
|
||||
bookmarks: "Bookmarks",
|
||||
bookmarksEmpty: "no bookmarks",
|
||||
},
|
||||
@ -118,7 +116,7 @@ const appStrings = {
|
||||
posts: {
|
||||
panelTitle: "Posts",
|
||||
noResults: "No results for the specified filters",
|
||||
noPostsError: "No posts found in archive (?!)",
|
||||
noPostsError: "No posts found in archive",
|
||||
},
|
||||
post: {
|
||||
by: "by",
|
||||
@ -140,6 +138,21 @@ const appStrings = {
|
||||
mentionsFilter: "Filter mentions",
|
||||
boostsFilter: "Filter boosted users",
|
||||
},
|
||||
tools: {
|
||||
panelTitle: "Tools",
|
||||
appSettings: "App settings",
|
||||
selectLanguage: "Select language",
|
||||
useDarkTheme: "Use dark theme",
|
||||
useLightTheme: "Use light theme",
|
||||
loadedFiles: "Loaded files",
|
||||
addAnother: "Add another archive",
|
||||
addAnotherTip:
|
||||
"Tip: You can open multiple archives at once.<br>You can also drag and drop your archive files anywhere on this window.",
|
||||
startOver: "Start over",
|
||||
startOverConfirm: "Discard current data and load a new archive file?",
|
||||
appLog: "App log",
|
||||
projectPage: `Project page (github)`,
|
||||
},
|
||||
},
|
||||
|
||||
fr: {
|
||||
@ -154,129 +167,142 @@ const appStrings = {
|
||||
p3: `<strong>Commencez par ouvrir votre archive avec MARL.</strong><br />
|
||||
Vous pouvez la glisser-déposer n'importe où sur cette page, ou
|
||||
{labelStart}cliquer ici pour la sélectionner{labelEnd}.`,
|
||||
projectPage: `Page du project (github)`,
|
||||
},
|
||||
misc: {
|
||||
loading: "Loading",
|
||||
closePanelBtn: "Close panel",
|
||||
loading: "Chargement",
|
||||
closePanelBtn: "Fermer le panneau",
|
||||
},
|
||||
menu: {
|
||||
profile: "Profile",
|
||||
filters: "Filters",
|
||||
filtersActive: "some filters are active",
|
||||
profile: "Profil",
|
||||
filters: "Filtres",
|
||||
filtersActive: "certains filtres sont actifs",
|
||||
tags: "Tags",
|
||||
newFile: "New File",
|
||||
newFileConfirm: "Discard current data and load a new archive file?",
|
||||
tools: "Outils",
|
||||
},
|
||||
lightbox: {
|
||||
next: "Next image",
|
||||
prev: "Previous image",
|
||||
close: "Close image",
|
||||
next: "Image suivante",
|
||||
prev: "Image précédente",
|
||||
close: "Fermer l'image",
|
||||
},
|
||||
actor: {
|
||||
accountInfo: "Account info",
|
||||
accounts: "Accounts",
|
||||
noAvatarImage: "No avatar image",
|
||||
noHeaderImage: "No header image",
|
||||
headerImage: "Header",
|
||||
memberSince: "Member since",
|
||||
accountInfo: "Infos du compte",
|
||||
accounts: "Comptes",
|
||||
noAvatarImage: "Pas d'avatar",
|
||||
noHeaderImage: "pas d'image d'en-tête",
|
||||
headerImage: "En-tête",
|
||||
memberSince: "Membre depuis",
|
||||
countPosts: "posts",
|
||||
countInArchive: "in archive",
|
||||
countDiffWhy: "Why are those two numbers different?",
|
||||
countDiffExplanation: `Posts that are not directly hosted on your instance are kept
|
||||
in a cache by your instance for a given time, after what they are deleted from that
|
||||
cache. Posts that are not in your instance cache any more are not included in your
|
||||
archive. This affects boosts, likes, and bookmarks.`,
|
||||
rawData: "Raw data {fileName}",
|
||||
favorites: "Favorites",
|
||||
favoritesEmpty: "no favorites",
|
||||
bookmarks: "Bookmarks",
|
||||
bookmarksEmpty: "no bookmarks",
|
||||
countInArchive: "dans l'archive",
|
||||
countDiffWhy: "Pourquoi ces deux nombres sont-ils différents ?",
|
||||
countDiffExplanation: `Les posts qui ne sont pas hébergés directement sur votre instance
|
||||
sont gardés en cache par celle-ci pour une durée limitée, après quoi ils sont supprimés
|
||||
de ce cache. Les posts qui ne sont plus présents dans le cache de votre instance ne sont
|
||||
pas inclus dans votre archive. Cela concerne les partages, les favoris et les marque-pages.`,
|
||||
rawData: "Données brutes {fileName}",
|
||||
likes: "Favoris",
|
||||
likesEmpty: "aucun favori",
|
||||
bookmarks: "Marque-pages",
|
||||
bookmarksEmpty: "aucun marque-page",
|
||||
},
|
||||
filters: {
|
||||
panelTitle: "Filter posts",
|
||||
panelNotice: `The list of posts will be automatically updated based on the active
|
||||
filters below.`,
|
||||
fullText: "Full text",
|
||||
panelTitle: "Filtrer les posts",
|
||||
panelNotice: `La liste des posts sera automatiquement mise à jour en fonction des filtres
|
||||
activés ci-dessous.`,
|
||||
fullText: "Partout",
|
||||
hashtagText: "Hashtags",
|
||||
mentionText: "Mentions",
|
||||
externalLink: "External links",
|
||||
summary: "Summary (CW)",
|
||||
isEdited: "Has been edited",
|
||||
isDuplicate: "Non-exact duplicates",
|
||||
externalLink: "Liens externes",
|
||||
summary: "Avertissement de contenu",
|
||||
isEdited: "A été modifié",
|
||||
isDuplicate: "Doublons imparfaits",
|
||||
|
||||
mustContain: "Must contain",
|
||||
mustContain: "Doit contenir",
|
||||
hasHashtags: "Hashtag(s)",
|
||||
hasMentions: "Mention(s)",
|
||||
hasExternalLink: "External link(s)",
|
||||
hasSummary: "Summary (CW)",
|
||||
hasExternalLink: "Lien(s) externe(s)",
|
||||
hasSummary: "Avertissement de contenu",
|
||||
|
||||
type: "Type",
|
||||
typeOriginal: "Original posts (incl. replies)",
|
||||
typeBoost: "Boosts",
|
||||
noStartingAt: 'Does not start with "@"',
|
||||
isSensitive: "Marked as sensitive",
|
||||
typeOriginal: "Posts originaux (y.c. réponses)",
|
||||
typeBoost: "Partages",
|
||||
noStartingAt: 'Ne commence pas par "@"',
|
||||
isSensitive: "Marqué comme sensible",
|
||||
|
||||
mustHaveAttachement: "Must have attachment",
|
||||
attachmentAny: "Any type",
|
||||
mustHaveAttachement: "Doit avoir un fichier joint",
|
||||
attachmentAny: "N'importe quel type",
|
||||
attachmentImage: "Image(s)",
|
||||
attachmentVideo: "Video(s)",
|
||||
attachmentSound: "Sound(s)",
|
||||
attachmentNoAltText: "Without alt text",
|
||||
attachmentWithAltText: "With alt text",
|
||||
attachmentVideo: "Vidéo(s)",
|
||||
attachmentSound: "Son(s)",
|
||||
attachmentNoAltText: "Sans description alternative",
|
||||
attachmentWithAltText: "Avec description alternative",
|
||||
|
||||
visibility: "Visibility",
|
||||
visibility: "Confidentialité",
|
||||
visibilityPublic: "Public",
|
||||
visibilityUnlisted: "Unlisted",
|
||||
visibilityFollowers: "Followers only",
|
||||
visibilityMentioned: "Mentioned people only",
|
||||
visibilityUnlisted: "Public discret",
|
||||
visibilityFollowers: "Abonnés",
|
||||
visibilityMentioned: "Personnes spécifiques",
|
||||
|
||||
language: "Language",
|
||||
author: "Author",
|
||||
language: "Langue",
|
||||
author: "Auteur",
|
||||
|
||||
resetFilters: "Reset filters",
|
||||
resetFilters: "Réinitialiser les filtres",
|
||||
},
|
||||
header: {
|
||||
countLabel: "posts",
|
||||
oldestFirst: "oldest first",
|
||||
latestFirst: "latest first",
|
||||
reverse: "Reverse",
|
||||
loadNewFile: "Load new file",
|
||||
oldestFirst: "les plus anciens d'abord",
|
||||
latestFirst: "les plus récents d'abord",
|
||||
reverse: "Inverser",
|
||||
loadNewFile: "Charger un nouveau fichier",
|
||||
},
|
||||
paging: {
|
||||
first: "First",
|
||||
prev: "Prev",
|
||||
next: "Next",
|
||||
last: "Last",
|
||||
pagingOptions: "Paging options",
|
||||
first: "Première",
|
||||
prev: "Précédente",
|
||||
next: "Suivante",
|
||||
last: "Dernière",
|
||||
pagingOptions: "Options de pagination",
|
||||
page: "Page",
|
||||
postsPerPage: "posts per page",
|
||||
reverseOrder: "Reverse order",
|
||||
postsPerPage: "posts par page",
|
||||
reverseOrder: "Inverser l'ordre",
|
||||
},
|
||||
posts: {
|
||||
panelTitle: "Posts",
|
||||
noResults: "No results for the specified filters",
|
||||
noPostsError: "No posts found in archive (?!)",
|
||||
noResults: "Pas de résultats pour les filtres spécifiés",
|
||||
noPostsError: "Aucun post trouvé dans l'archive",
|
||||
},
|
||||
post: {
|
||||
by: "by",
|
||||
lastUpdated: "Last updated",
|
||||
linkToPost: "link",
|
||||
attachmentNoAlt: "No description provided",
|
||||
attachmentInArchive: "In archive:",
|
||||
people: "People",
|
||||
by: "par",
|
||||
lastUpdated: "Dernière modification",
|
||||
linkToPost: "lien",
|
||||
attachmentNoAlt: "Aucune description fournie",
|
||||
attachmentInArchive: "Dans l'archive :",
|
||||
people: "Personnes",
|
||||
hashtags: "Hashtags",
|
||||
extLinks: "External links",
|
||||
rawData: "Raw data",
|
||||
extLinks: "Liens externes",
|
||||
rawData: "Données brutes",
|
||||
},
|
||||
tags: {
|
||||
panelTitle: "Tags",
|
||||
hashtags: "Hashtags",
|
||||
mentions: "Mentions",
|
||||
boosts: "Boosted users",
|
||||
hashtagsFilter: "Filter hashtags",
|
||||
mentionsFilter: "Filter mentions",
|
||||
boostsFilter: "Filter boosted users",
|
||||
boosts: "Utilisateurs partagés",
|
||||
hashtagsFilter: "Filtrer les hashtags",
|
||||
mentionsFilter: "Filtrer les mentions",
|
||||
boostsFilter: "Filter utilisateurs partagés",
|
||||
},
|
||||
tools: {
|
||||
panelTitle: "Outils",
|
||||
appSettings: "Réglages de l'app",
|
||||
selectLanguage: "Choisir la langue",
|
||||
useDarkTheme: "Utiliser le thème sombre",
|
||||
useLightTheme: "Utiliser le thème clair",
|
||||
loadedFiles: "Fichiers chargés",
|
||||
addAnother: "Ajouter une autre archive",
|
||||
addAnotherTip:
|
||||
"Astuce: Vous pouvez ouvrir plusieurs archives en même temps.<br>Vous pouvez aussi glisser-déposer vos fichiers d'archive n'importe où dans cette fenêtre.",
|
||||
startOver: "Recommencer",
|
||||
startOverConfirm: "Repartir de zéro et charger un nouveau fichier ?",
|
||||
appLog: "Journal",
|
||||
projectPage: `Page du project (github)`,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
678
dist/js/utils.js
vendored
Normal file
678
dist/js/utils.js
vendored
Normal file
@ -0,0 +1,678 @@
|
||||
function resetStores() {
|
||||
Alpine.store("files").resetState();
|
||||
Alpine.store("lightbox").resetState();
|
||||
Alpine.store("ui").resetState();
|
||||
}
|
||||
|
||||
function unZip(files) {
|
||||
const firstLoad = Alpine.store("files").sources.length === 0;
|
||||
if (firstLoad) {
|
||||
resetStores();
|
||||
}
|
||||
Alpine.store("files").loading = true;
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
|
||||
if (
|
||||
Alpine.store("files").sources.some((source) => {
|
||||
return (
|
||||
source.fileInfos.name === file.name &&
|
||||
source.fileInfos.size === file.size &&
|
||||
source.fileInfos.lastModified === file.lastModified
|
||||
);
|
||||
})
|
||||
) {
|
||||
const msg = `File already loaded: <b>${file.name}</b>`;
|
||||
console.warn(msg);
|
||||
Alpine.store("ui").logMsg(msg, "warn");
|
||||
continue;
|
||||
}
|
||||
|
||||
Alpine.store("ui").logMsg(`Loading file: <b>${file.name}</b>`, "info");
|
||||
|
||||
JSZip.loadAsync(file).then(
|
||||
(content) => {
|
||||
const index = Alpine.store("files").sources.length;
|
||||
const fileInfos = {
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
lastModified: file.lastModified,
|
||||
};
|
||||
|
||||
Alpine.store("files").sources[index] = {
|
||||
id: index,
|
||||
fileInfos: fileInfos,
|
||||
nbToots: 0,
|
||||
|
||||
actor: {},
|
||||
outbox: {},
|
||||
likes: [],
|
||||
bookmarks: [],
|
||||
avatar: {},
|
||||
header: {},
|
||||
|
||||
loaded: {
|
||||
actor: false,
|
||||
avatar: false,
|
||||
header: false,
|
||||
outbox: false,
|
||||
likes: false,
|
||||
bookmarks: false,
|
||||
},
|
||||
};
|
||||
|
||||
Alpine.store("files").sources[index]._raw = content.files;
|
||||
|
||||
loadJsonFile("actor", index, fileInfos);
|
||||
loadJsonFile("outbox", index, fileInfos);
|
||||
loadJsonFile("likes", index, fileInfos);
|
||||
loadJsonFile("bookmarks", index, fileInfos);
|
||||
},
|
||||
(error) => {
|
||||
const msg = `Error loading <b>${file.name}</b>: ${error.message}`;
|
||||
console.error(msg);
|
||||
Alpine.store("ui").logMsg(msg, "error");
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function loadJsonFile(name, index, fileInfos) {
|
||||
const content = Alpine.store("files").sources[index]._raw;
|
||||
|
||||
if (content[name + ".json"] === undefined) {
|
||||
if (name === "likes" || name === "bookmarks") {
|
||||
// we can still run the app without those files
|
||||
const msg = `<b>${fileInfos.name}</b>: File ${name}.json not found in archive.`;
|
||||
console.warn(msg);
|
||||
Alpine.store("ui").logMsg(msg, "warn");
|
||||
Alpine.store("files").sources[index].loaded[name] = true;
|
||||
} else {
|
||||
// this should NOT happen and will prevent the app from running
|
||||
const msg = `<b>Critical error - ${fileInfos.name}</b>: File ${name}.json not found in archive.`;
|
||||
console.error(msg);
|
||||
Alpine.store("ui").logMsg(msg, "error");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
content[name + ".json"].async("text").then(function (txt) {
|
||||
if (name === "actor") {
|
||||
Alpine.store("files").sources[index].actor = JSON.parse(txt);
|
||||
loadActorImages(index);
|
||||
Alpine.store("files").sources[index].loaded.actor = true;
|
||||
} // actor.json
|
||||
|
||||
if (name === "outbox") {
|
||||
let data = JSON.parse(txt);
|
||||
|
||||
let toots = data.orderedItems.reduce((accu, t) => {
|
||||
let t2 = preprocessToots(t, index);
|
||||
if (t2) {
|
||||
accu.push(t2);
|
||||
}
|
||||
return accu;
|
||||
}, []);
|
||||
|
||||
Alpine.store("files").toots = Alpine.store("files").toots.concat(toots);
|
||||
Alpine.store("files").sources[index].nbToots = toots.length;
|
||||
delete data.orderedItems;
|
||||
Alpine.store("files").sources[index].outbox = data;
|
||||
Alpine.store("files").sources[index].loaded.outbox = true;
|
||||
} // outbox.json
|
||||
|
||||
if (name === "likes" || name === "bookmarks") {
|
||||
const tmp = JSON.parse(txt);
|
||||
Alpine.store("files").sources[index][name] = tmp.orderedItems;
|
||||
Alpine.store("files").sources[index].loaded[name] = true;
|
||||
} // likes.json || bookmarks.json
|
||||
});
|
||||
}
|
||||
|
||||
function buildTootsInfos() {
|
||||
let langs = {};
|
||||
let boosts = [];
|
||||
|
||||
if (Alpine.store("files").toots.length > 0) {
|
||||
let infos = Alpine.store("files").toots.reduce(
|
||||
(accu, toot) => {
|
||||
for (let lang in toot._marl.langs) {
|
||||
const l = toot._marl.langs[lang];
|
||||
if (!accu.langs[l]) {
|
||||
accu.langs[l] = 1;
|
||||
} else {
|
||||
accu.langs[l]++;
|
||||
}
|
||||
}
|
||||
|
||||
if (toot.type === "Announce") {
|
||||
// since Mastodon doesn't allow (yet?) cross-origin requests to
|
||||
// retrieve post data (for boosts), we try to at least extract the
|
||||
// user names for all the boosts contained in the archive
|
||||
|
||||
// [ISSUE] "object" value is a string most of the times, but
|
||||
// sometimes it's a complex object similar to type "Create"
|
||||
if (typeof toot.object === "object" && toot.object !== null) {
|
||||
// let's ignore this case for now...
|
||||
// [TODO], but not clear how it should be handled
|
||||
} else if (toot.object) {
|
||||
// if it's not an object and it has a value, then it's simply a
|
||||
// url (string) pointing to the original (boosted) post.
|
||||
// [ISSUE] URL format not always consistent... (esp. in the case
|
||||
// of non-Mastodon instances) - e.g:
|
||||
// https://craftopi.art/objects/[...]
|
||||
// https://firefish.city/notes/[...]
|
||||
// https://bsky.brid.gy/convert/ap/at://did:plc:[...]/app.bsky.feed.post/[...]
|
||||
// -> the user name is not always present in URL
|
||||
const url = toot.object.split("/");
|
||||
let name;
|
||||
let user;
|
||||
let domain;
|
||||
if (url.length > 2) {
|
||||
domain = url[2];
|
||||
|
||||
if (url[0] === "https:" && url[3] === "users" && url[5] === "statuses") {
|
||||
// Mastodon URL format -> user name
|
||||
name = url[4];
|
||||
user = `https://${url[2]}/users/${url[4]}/`;
|
||||
} else {
|
||||
// other URL format -> domain name
|
||||
name = `? ${url[2]}`;
|
||||
user = `https://${url[2]}/`;
|
||||
}
|
||||
|
||||
if (!accu.boosts[name]) {
|
||||
accu.boosts[name] = {
|
||||
nb: 1,
|
||||
name: name,
|
||||
url: user,
|
||||
domain: domain,
|
||||
};
|
||||
} else {
|
||||
accu.boosts[name].nb++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return accu;
|
||||
},
|
||||
{ langs: {}, boosts: {} }
|
||||
);
|
||||
|
||||
langs = infos.langs;
|
||||
|
||||
boosts = [];
|
||||
for (var key in infos.boosts) {
|
||||
boosts.push(infos.boosts[key]);
|
||||
}
|
||||
}
|
||||
|
||||
Alpine.store("files").languages = langs;
|
||||
Alpine.store("files").boostsAuthors = boosts;
|
||||
}
|
||||
|
||||
function buildDynamicFilters() {
|
||||
for (const lang in Alpine.store("files").languages) {
|
||||
Alpine.store("files").filtersDefault["lang_" + lang] = true;
|
||||
}
|
||||
|
||||
for (const source of Alpine.store("files").sources) {
|
||||
Alpine.store("files").filtersDefault["actor_" + source.id] = true;
|
||||
}
|
||||
|
||||
Alpine.store("files").resetFilters(false);
|
||||
}
|
||||
|
||||
function preprocessToots(t, index) {
|
||||
// build the '_marl' prop for each toot
|
||||
let marl = {
|
||||
langs: [],
|
||||
source: index,
|
||||
};
|
||||
|
||||
// check for duplicates (in case of multiple archive files)
|
||||
if (Alpine.store("files").toc.includes(t.id)) {
|
||||
const alts = Alpine.store("files").toots.filter((t2) => t2.id === t.id);
|
||||
|
||||
let identical = false;
|
||||
const flat1 = JSON.stringify(t);
|
||||
|
||||
alts.forEach((alt) => {
|
||||
let alt2 = JSON.parse(JSON.stringify(alt));
|
||||
delete alt2._marl;
|
||||
const flat2 = JSON.stringify(alt2);
|
||||
|
||||
if (flat1 === flat2) {
|
||||
identical = true;
|
||||
} else {
|
||||
alt._marl.duplicate = true;
|
||||
marl.duplicate = true;
|
||||
Alpine.store("files").duplicates = true;
|
||||
}
|
||||
});
|
||||
if (identical) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
Alpine.store("files").toc.push(t.id);
|
||||
}
|
||||
|
||||
if (t.type === "Create") {
|
||||
if (typeof t.object === "object" && t.object !== null && t.object.contentMap) {
|
||||
let langs = [];
|
||||
for (let lang in t.object.contentMap) {
|
||||
langs.push(lang);
|
||||
}
|
||||
marl.langs = langs;
|
||||
} else {
|
||||
marl.langs = ["undefined"];
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof t.object === "object" && t.object !== null) {
|
||||
if (t.object.content) {
|
||||
const content = t.object.content.toLowerCase();
|
||||
marl.textContent = stripHTML(content);
|
||||
marl.externalLinks = extractExternalLinks(content);
|
||||
}
|
||||
if (t.object.summary) {
|
||||
marl.summary = t.object.summary.toLowerCase();
|
||||
}
|
||||
|
||||
if (t.object.attachment && t.object.attachment.length) {
|
||||
marl.hasAttachments = true;
|
||||
}
|
||||
} else if (t.object) {
|
||||
marl.textContent = t.object.toLowerCase();
|
||||
}
|
||||
|
||||
marl.visibility = tootVisibility(t);
|
||||
|
||||
const id = t.id.split("/");
|
||||
marl.id = id[id.length - 2];
|
||||
|
||||
t._marl = marl;
|
||||
return t;
|
||||
}
|
||||
|
||||
function loadActorImages(index) {
|
||||
const actor = Alpine.store("files").sources[index].actor;
|
||||
const content = Alpine.store("files").sources[index]._raw;
|
||||
|
||||
if (actor.icon && actor.icon.type === "Image" && actor.icon.url && content[actor.icon.url]) {
|
||||
const image = actor.icon;
|
||||
content[image.url].async("base64").then(function (content) {
|
||||
Alpine.store("files").sources[index].avatar = {
|
||||
type: image.mediaType,
|
||||
content: content,
|
||||
noImg: false,
|
||||
};
|
||||
Alpine.store("files").sources[index].loaded.avatar = true;
|
||||
});
|
||||
} else {
|
||||
Alpine.store("files").sources[index].avatar = { noImg: true };
|
||||
Alpine.store("files").sources[index].loaded.avatar = true;
|
||||
}
|
||||
|
||||
if (actor.image && actor.image.type === "Image" && actor.image.url && content[actor.image.url]) {
|
||||
const image = actor.image;
|
||||
content[image.url].async("base64").then(function (content) {
|
||||
Alpine.store("files").sources[index].header = {
|
||||
type: image.mediaType,
|
||||
content: content,
|
||||
noImg: false,
|
||||
};
|
||||
Alpine.store("files").sources[index].loaded.header = true;
|
||||
});
|
||||
} else {
|
||||
Alpine.store("files").sources[index].header = { noImg: true };
|
||||
Alpine.store("files").sources[index].loaded.header = true;
|
||||
}
|
||||
}
|
||||
|
||||
function setHueForSources() {
|
||||
const nbSources = Alpine.store("files").sources.length;
|
||||
const hueStart = Math.round(Math.random() * 360); // MARL accent: 59.17
|
||||
const hueSpacing = Math.round(360 / nbSources);
|
||||
|
||||
for (let i = 0; i < nbSources; i++) {
|
||||
Alpine.store("files").sources[i].hue = hueStart + hueSpacing * i;
|
||||
}
|
||||
}
|
||||
|
||||
function checkAppReady(ok) {
|
||||
if (ok) {
|
||||
buildTootsInfos();
|
||||
buildDynamicFilters();
|
||||
cleanUpRaw();
|
||||
setHueForSources();
|
||||
document.getElementById("main-section").focus();
|
||||
Alpine.store("ui").checkMenuState();
|
||||
Alpine.store("files").sortToots();
|
||||
Alpine.store("files").loading = false;
|
||||
Alpine.store("files").someFilesLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
function cleanUpRaw() {
|
||||
for (let i = 0; i < Alpine.store("files").sources.length; i++) {
|
||||
const content = Alpine.store("files").sources[i]._raw;
|
||||
if (content.cleanedUp) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const actor = Alpine.store("files").sources[i].actor;
|
||||
if (actor.image && actor.image.url) {
|
||||
delete content[actor.image.url];
|
||||
}
|
||||
if (actor.icon && actor.icon.url) {
|
||||
delete content[actor.icon.url];
|
||||
}
|
||||
delete content["actor.json"];
|
||||
delete content["outbox.json"];
|
||||
delete content["likes.json"];
|
||||
delete content["bookmarks.json"];
|
||||
content.cleanedUp = true;
|
||||
|
||||
Alpine.store("files").sources[i]._raw = content;
|
||||
}
|
||||
}
|
||||
|
||||
function loadAttachedMedia(att, index) {
|
||||
if (attachmentIsImage(att) || attachmentIsVideo(att) || attachmentIsSound(att)) {
|
||||
const data = Alpine.store("files").sources[index]._raw;
|
||||
let url = att.url;
|
||||
// ?! some instances seem to add their own name in front of the path,
|
||||
// resulting in an invalid path with relation to the archive
|
||||
// structure (e.g. "/framapiaf/media_attachments/...", but in the
|
||||
// archive there is only a folder "/media_attachments")
|
||||
// => So we remove everything that comes before "media_attachments/",
|
||||
// hoping it doesn't break something else... :/
|
||||
const prefix = url.indexOf("media_attachments/");
|
||||
if (prefix > 0) {
|
||||
url = url.slice(prefix);
|
||||
}
|
||||
if (!data[url]) {
|
||||
return;
|
||||
}
|
||||
data[url].async("base64").then((content) => {
|
||||
Alpine.store("files").sources[index][att.url] = {
|
||||
type: att.mediaType,
|
||||
content: content,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function pagingUpdated() {
|
||||
document.querySelectorAll(`#toots details[open]`).forEach((e) => {
|
||||
e.removeAttribute("open");
|
||||
});
|
||||
}
|
||||
|
||||
function scrollTootsToTop(setFocusTo) {
|
||||
setTimeout(() => {
|
||||
document.getElementById("toots").scrollTop = 0;
|
||||
if (setFocusTo) {
|
||||
// for keyboard users: we transfer the focus to the corresponding button
|
||||
// in the upper paging module; or, in the cases where said button is
|
||||
// disabled, we set the focus on the list of posts.
|
||||
document.getElementById(setFocusTo).focus();
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
|
||||
function contentType(data) {
|
||||
let r = "";
|
||||
switch (data) {
|
||||
case "Create":
|
||||
r = "Post";
|
||||
break;
|
||||
case "Announce":
|
||||
r = "Boost";
|
||||
break;
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
function tootVisibility(data) {
|
||||
if (data.to.includes("https://www.w3.org/ns/activitystreams#Public")) {
|
||||
return ["public", AlpineI18n.t("filters.visibilityPublic")];
|
||||
}
|
||||
if (
|
||||
data.to.some((x) => x.indexOf("/followers") > -1) &&
|
||||
!data.to.includes("https://www.w3.org/ns/activitystreams#Public") &&
|
||||
data.cc.includes("https://www.w3.org/ns/activitystreams#Public")
|
||||
) {
|
||||
return ["unlisted", AlpineI18n.t("filters.visibilityUnlisted")];
|
||||
}
|
||||
if (
|
||||
data.to.some((x) => x.indexOf("/followers") > -1) &&
|
||||
!data.to.includes("https://www.w3.org/ns/activitystreams#Public") &&
|
||||
!data.cc.includes("https://www.w3.org/ns/activitystreams#Public")
|
||||
) {
|
||||
return ["followers", AlpineI18n.t("filters.visibilityFollowers")];
|
||||
}
|
||||
if (
|
||||
!data.to.some((x) => x.indexOf("/followers") > -1) &&
|
||||
!data.to.includes("https://www.w3.org/ns/activitystreams#Public") &&
|
||||
!data.cc.includes("https://www.w3.org/ns/activitystreams#Public")
|
||||
) {
|
||||
return ["mentioned", AlpineI18n.t("filters.visibilityMentioned")];
|
||||
}
|
||||
}
|
||||
|
||||
function tootHasTags(toot) {
|
||||
return typeof toot.object === "object" && toot.object !== null && toot.object.tag && toot.object.tag.length;
|
||||
}
|
||||
|
||||
function formatJson(data) {
|
||||
let r = data;
|
||||
if (r._marl) {
|
||||
// not a part of the source data; let's hide it to avoid confusion
|
||||
r = JSON.parse(JSON.stringify(data));
|
||||
delete r._marl;
|
||||
}
|
||||
return JSON.stringify(r, null, 4);
|
||||
}
|
||||
|
||||
function formatAuthor(author, plainText) {
|
||||
if (plainText) {
|
||||
return author.split("/").pop();
|
||||
} else {
|
||||
return `<a href="${author}" target="_blank">${author.split("/").pop()}</a>`;
|
||||
}
|
||||
}
|
||||
|
||||
function formatDateTime(data) {
|
||||
let date = new Date(data);
|
||||
const dateOptions = {
|
||||
weekday: "long",
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
};
|
||||
return date.toLocaleDateString(Alpine.store("ui").lang, dateOptions);
|
||||
}
|
||||
|
||||
function formatFileDateTime(data) {
|
||||
let date = new Date(data);
|
||||
const dateOptions = {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour12: false,
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
};
|
||||
return date.toLocaleDateString(Alpine.store("ui").lang, dateOptions);
|
||||
}
|
||||
function formatFileSize(size) {
|
||||
var i = size == 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024));
|
||||
return +(size / Math.pow(1024, i)).toFixed(2) * 1 + " " + ["B", "kB", "MB", "GB", "TB"][i]; // ### i18n
|
||||
}
|
||||
|
||||
function formatDate(data) {
|
||||
let date = new Date(data);
|
||||
const dateOptions = {
|
||||
weekday: "long",
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
};
|
||||
return date.toLocaleDateString(Alpine.store("ui").lang, dateOptions);
|
||||
}
|
||||
|
||||
function formatNumber(nb) {
|
||||
return nb.toLocaleString();
|
||||
}
|
||||
|
||||
function formatLikesBookmarks(url) {
|
||||
const u = url.split("/");
|
||||
u.splice(0, 2);
|
||||
|
||||
// 0 [domain]
|
||||
// 1 "users"
|
||||
// 2 [username]
|
||||
// 3 "statuses"
|
||||
// 4 [post id]
|
||||
|
||||
let text = `<span class="url-instance">${u[0]}</span>`;
|
||||
if (u[1] === "users" && u[3] === "statuses") {
|
||||
text += `<span class="url-actor">${u[2]}</span><span class="url-post-id">${u[4]}</span>`;
|
||||
} else {
|
||||
u.splice(0, 1);
|
||||
text += `<span class="url-post-id">${u.join("/")}</span>`;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
function stripHTML(str) {
|
||||
let doc = new DOMParser().parseFromString(str, "text/html");
|
||||
return doc.body.textContent || "";
|
||||
}
|
||||
|
||||
function extractExternalLinks(str) {
|
||||
const doc = new DOMParser().parseFromString(str, "text/html");
|
||||
const nodes = doc.querySelectorAll("a[href]:not(.mention)");
|
||||
let links = [];
|
||||
nodes.forEach((link) => {
|
||||
links.push({
|
||||
href: link.href,
|
||||
text: link.textContent,
|
||||
});
|
||||
});
|
||||
return links;
|
||||
}
|
||||
|
||||
function attachmentIsImage(att) {
|
||||
return att.mediaType === "image/jpeg" || att.mediaType === "image/png";
|
||||
}
|
||||
|
||||
function attachmentIsVideo(att) {
|
||||
return att.mediaType === "video/mp4";
|
||||
}
|
||||
|
||||
function attachmentIsSound(att) {
|
||||
return att.mediaType === "audio/mpeg";
|
||||
}
|
||||
|
||||
function attachmentWrapperClass(att) {
|
||||
let r = [];
|
||||
if (attachmentIsImage(att)) {
|
||||
r.push("att-img");
|
||||
} else if (attachmentIsSound(att)) {
|
||||
r.push("att-sound");
|
||||
} else if (attachmentIsVideo(att)) {
|
||||
r.push("att-video");
|
||||
}
|
||||
|
||||
if (!att.name) {
|
||||
r.push("no-alt-text");
|
||||
}
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
function isFilterActive(name) {
|
||||
return Alpine.store("files").filters[name] !== Alpine.store("files").filtersDefault[name];
|
||||
}
|
||||
|
||||
function startOver() {
|
||||
const txt = AlpineI18n.t("tools.startOverConfirm");
|
||||
if (confirm(txt)) {
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
function detectLangFromBrowser() {
|
||||
const langs = navigator.languages;
|
||||
if (langs && langs.length) {
|
||||
for (let i = 0; i < langs.length; i++) {
|
||||
let lang = langs[i].split("-")[0];
|
||||
if (Alpine.store("ui").appLangs[lang]) {
|
||||
const msg = `Setting language based on browser preference: <b>'${lang}' (${
|
||||
Alpine.store("ui").appLangs[lang]
|
||||
})</b>`;
|
||||
Alpine.store("ui").logMsg(msg, "info");
|
||||
return lang;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function setLang() {
|
||||
const lang = Alpine.store("ui").lang;
|
||||
AlpineI18n.locale = lang;
|
||||
Alpine.store("userPrefs").save("lang", lang);
|
||||
|
||||
const msg = `App language set to <b>'${lang}' (${Alpine.store("ui").appLangs[lang]})</b>`;
|
||||
Alpine.store("ui").logMsg(msg);
|
||||
}
|
||||
|
||||
function setTheme(theme) {
|
||||
document.getElementsByTagName("html")[0].setAttribute("class", theme);
|
||||
}
|
||||
|
||||
// drag'n'drop over entire page
|
||||
|
||||
const drag = {
|
||||
el: null,
|
||||
|
||||
init(el) {
|
||||
this.dropArea = document.getElementById(el);
|
||||
|
||||
["dragenter", "dragover", "dragleave", "drop"].forEach((eventName) => {
|
||||
this.dropArea.addEventListener(eventName, (e) => this.preventDragDefaults(e), false);
|
||||
});
|
||||
["dragenter", "dragover"].forEach((eventName) => {
|
||||
this.dropArea.addEventListener(eventName, () => this.highlightDrag(), false);
|
||||
});
|
||||
["dragleave", "drop"].forEach((eventName) => {
|
||||
this.dropArea.addEventListener(eventName, () => this.unhighlightDrag(), false);
|
||||
});
|
||||
this.dropArea.addEventListener("drop", (e) => this.handleDrop(e), false);
|
||||
},
|
||||
|
||||
preventDragDefaults(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
},
|
||||
highlightDrag() {
|
||||
this.dropArea.classList.add("highlight-drag");
|
||||
},
|
||||
unhighlightDrag() {
|
||||
this.dropArea.classList.remove("highlight-drag");
|
||||
},
|
||||
handleDrop(e) {
|
||||
const dt = e.dataTransfer;
|
||||
const files = dt.files;
|
||||
unZip(files);
|
||||
},
|
||||
};
|
@ -121,6 +121,9 @@ Mastodon: https://lou.lt/@s427
|
||||
|
||||
## Version history
|
||||
|
||||
- v. 2.1
|
||||
- Internationalization: MARL is now usable in two languages (English and French), with support for more languages. Get in touch if you want to help translating MARL in your language!
|
||||
- Tools panel: a new "Tools" button will a new panel, where you can change various settings for the app (language, theme) as well as see information about your loaded files, messages from the app, and an "about" section.
|
||||
- v. 2.0
|
||||
- code refactoring: MARL now uses Astro to build its HTML code as well as optimize and bundle its assets (CSS, SVG, images).
|
||||
- all assets used to build MARL are now stored in the `dev` folder.
|
||||
|
Loading…
x
Reference in New Issue
Block a user