mirror of
https://github.com/s427/MARL.git
synced 2025-01-31 03:24:48 +01:00
Merge branch 'localdev' - support for multiple archives (issue #3)
This commit is contained in:
commit
2d6b8d09ba
166
css/_actor.scss
166
css/_actor.scss
@ -1,16 +1,18 @@
|
||||
.actor {
|
||||
container-type: inline-size;
|
||||
height: 100dvh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
color: var(--fg-inv1);
|
||||
background: var(--bg-inv3);
|
||||
position: relative;
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 1;
|
||||
width: 2rem;
|
||||
box-shadow: -1.5rem 0 2rem -2rem #000 inset;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--fg-inv0);
|
||||
color: var(--actor-fg0);
|
||||
}
|
||||
h1,
|
||||
h2,
|
||||
@ -22,13 +24,13 @@
|
||||
summary {
|
||||
padding: 1rem;
|
||||
svg {
|
||||
fill: var(--fg-inv1);
|
||||
fill: var(--actor-fg1);
|
||||
}
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: var(--fg-inv0);
|
||||
color: var(--actor-fg0);
|
||||
svg {
|
||||
fill: var(--fg-inv0);
|
||||
fill: var(--actor-fg0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -39,7 +41,28 @@
|
||||
}
|
||||
}
|
||||
|
||||
.actors-wrapper {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.actor-panel {
|
||||
flex-grow: 1;
|
||||
height: 100%;
|
||||
container-type: inline-size;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
color: var(--actor-fg1);
|
||||
background: var(--actor-bg3);
|
||||
}
|
||||
|
||||
.actor-pretty {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 0;
|
||||
padding: 1rem 1rem 2rem;
|
||||
}
|
||||
@ -118,13 +141,13 @@
|
||||
.actor-summary {
|
||||
margin: 2rem 0;
|
||||
padding: 1rem;
|
||||
color: var(--fg-inv0);
|
||||
color: var(--actor-fg0);
|
||||
line-height: 1.3;
|
||||
background: var(--bg-inv4);
|
||||
background: var(--actor-bg4);
|
||||
background: radial-gradient(
|
||||
circle at 200% 150%,
|
||||
var(--bg-inv3),
|
||||
var(--bg-inv4)
|
||||
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);
|
||||
@ -146,8 +169,8 @@
|
||||
}
|
||||
dd {
|
||||
padding: 0.5rem 1rem;
|
||||
color: var(--fg-inv0);
|
||||
border-left: 1px solid var(--bg-inv4);
|
||||
color: var(--actor-fg0);
|
||||
border-left: 1px solid var(--actor-bg4);
|
||||
}
|
||||
|
||||
@container (width < 340px) {
|
||||
@ -156,7 +179,7 @@
|
||||
dl {
|
||||
display: block;
|
||||
padding: 0.5rem 1rem;
|
||||
border-bottom: 1px solid var(--bg-inv4);
|
||||
border-bottom: 1px solid var(--actor-bg4);
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
@ -180,7 +203,7 @@
|
||||
gap: 1rem;
|
||||
.count {
|
||||
display: block;
|
||||
color: var(--fg-inv0);
|
||||
color: var(--actor-fg0);
|
||||
font-size: 1.6em;
|
||||
font-weight: bold;
|
||||
}
|
||||
@ -200,11 +223,11 @@
|
||||
font-weight: bold;
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
color: var(--fg-inv0);
|
||||
color: var(--actor-fg0);
|
||||
}
|
||||
}
|
||||
&[open] summary {
|
||||
color: var(--fg-inv0);
|
||||
color: var(--actor-fg0);
|
||||
}
|
||||
p {
|
||||
margin-top: 0.5rem;
|
||||
@ -214,7 +237,7 @@
|
||||
}
|
||||
|
||||
.actor-raw {
|
||||
background: var(--bg-inv2);
|
||||
background: var(--actor-bg2);
|
||||
box-shadow: 0 0.75rem 0.75rem -1rem #000 inset;
|
||||
.details-content {
|
||||
padding: 1rem 1rem 2rem;
|
||||
@ -224,8 +247,7 @@
|
||||
}
|
||||
|
||||
.actor-likes-bookmarks {
|
||||
flex-grow: 1;
|
||||
background: var(--bg-inv0);
|
||||
background: var(--actor-bg0);
|
||||
overflow-wrap: break-word;
|
||||
|
||||
h2 {
|
||||
@ -260,10 +282,100 @@
|
||||
}
|
||||
|
||||
.actor-likes {
|
||||
background: var(--bg-inv1);
|
||||
background: var(--actor-bg1);
|
||||
box-shadow: 0 0.75rem 0.75rem -1rem #000 inset;
|
||||
}
|
||||
.actor-bookmarks {
|
||||
padding-bottom: 1rem;
|
||||
box-shadow: 0 0.75rem 0.75rem -1rem #000 inset;
|
||||
}
|
||||
|
||||
.multiple-actors {
|
||||
.actors-tabs {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem 1rem;
|
||||
background-color: var(--actor-tabs-bg);
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
|
||||
button {
|
||||
all: unset;
|
||||
padding: 0.35rem 0.7rem 0.4rem;
|
||||
font-size: 0.85em;
|
||||
font-family: inherit;
|
||||
border-radius: 0.25rem;
|
||||
color: #fff;
|
||||
background: var(--actor-accent2-ok, transparent);
|
||||
cursor: pointer;
|
||||
span {
|
||||
display: inline-block;
|
||||
padding: 0 0 0.3rem;
|
||||
border-bottom: 0.1rem solid transparent;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
span {
|
||||
border-bottom-color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
}
|
||||
&[aria-selected="true"] {
|
||||
box-shadow: none;
|
||||
span {
|
||||
border-bottom-color: #fff;
|
||||
}
|
||||
}
|
||||
&:focus-visible {
|
||||
outline: 2px solid hsl(var(--actor-hue), 50%, 80%);
|
||||
outline: 2px solid var(--actor-accent-ok, var(--actor-accent));
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// color variations
|
||||
|
||||
.actor {
|
||||
*:focus-visible {
|
||||
outline-color: var(--actor-accent-ok, var(--accent-light));
|
||||
}
|
||||
}
|
||||
|
||||
.actor summary svg {
|
||||
fill: var(--actor-fg1-ok, var(--actor-fg1));
|
||||
}
|
||||
.actor-panel {
|
||||
color: var(--actor-fg1-ok, var(--actor-fg1));
|
||||
background: var(--actor-bg3-ok, var(--actor-bg3));
|
||||
}
|
||||
.actor-summary {
|
||||
background: var(--actor-bg4-ok, var(--actor-bg4));
|
||||
background: radial-gradient(
|
||||
circle at 200% 150%,
|
||||
var(--actor-bg3-ok, var(--actor-bg3)),
|
||||
var(--actor-bg4-ok, var(--actor-bg4))
|
||||
);
|
||||
}
|
||||
.actor-infos {
|
||||
dd {
|
||||
border-left-color: var(--actor-bg4-ok, var(--actor-bg4));
|
||||
}
|
||||
@container (width < 340px) {
|
||||
dl {
|
||||
border-bottom-color: var(--actor-bg4-ok, var(--actor-bg4));
|
||||
}
|
||||
}
|
||||
}
|
||||
.actor-raw {
|
||||
background: var(--actor-bg2-ok, var(--actor-bg2));
|
||||
}
|
||||
.actor-likes-bookmarks {
|
||||
background: var(--actor-bg0-ok, var(--actor-bg0));
|
||||
}
|
||||
.actor-likes {
|
||||
background: var(--actor-bg1-ok, var(--actor-bg1));
|
||||
}
|
||||
}
|
||||
|
156
css/_colors.scss
156
css/_colors.scss
@ -1,64 +1,80 @@
|
||||
// tint and saturation factor for the app color scheme
|
||||
$hue: 30;
|
||||
$sat: 0.5;
|
||||
$hue: 30; // === 59.17 in oklch
|
||||
|
||||
/*********************************/
|
||||
/* global background/text colors */
|
||||
/*********************************/
|
||||
|
||||
$bg0: hsl(var(--hue), calc(var(--sat) * 0%), 100%);
|
||||
// -> post content
|
||||
$bg1: hsl(var(--hue), calc(var(--sat) * 20%), 95%);
|
||||
// -> post
|
||||
$bg2: hsl(var(--hue), calc(var(--sat) * 30%), 90%);
|
||||
// -> post attachment/meta/raw
|
||||
$bg3: hsl(var(--hue), calc(var(--sat) * 35%), 87%);
|
||||
// -> posts header
|
||||
$bg4: hsl(var(--hue), calc(var(--sat) * 40%), 84%);
|
||||
// -> filters/tags panels
|
||||
$bg0: #fff; // post content
|
||||
$bg1: hsl($hue, 10%, 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
|
||||
|
||||
$fg0: hsl(var(--hue), calc(var(--sat) * 0%), 0%);
|
||||
// -> post content
|
||||
$fg1: hsl(var(--hue), calc(var(--sat) * 20%), 20%);
|
||||
// -> body text; some SVG icons (panel close)
|
||||
$fg2: hsl(var(--hue), calc(var(--sat) * 20%), 30%);
|
||||
// -> headings (h1, h2...)
|
||||
$fg0: #000; // post content
|
||||
$fg1: hsl($hue, 10%, 20%); // body text; some SVG icons (panel close)
|
||||
$fg2: hsl($hue, 10%, 30%); // headings (h1, h2...)
|
||||
|
||||
/********************************************/
|
||||
/* inverted light scheme (e.g. actor panel) */
|
||||
/********************************************/
|
||||
$fg-inv: #fff; // misc highlighted text
|
||||
|
||||
$bg-inv0: hsl(var(--hue), calc(var(--sat) * 75%), 11%);
|
||||
// -> bookmarks
|
||||
$bg-inv1: hsl(var(--hue), calc(var(--sat) * 65%), 15%);
|
||||
// -> favorites
|
||||
$bg-inv2: hsl(var(--hue), calc(var(--sat) * 50%), 20%);
|
||||
// -> actor raw data, mobile menu
|
||||
$bg-inv3: hsl(var(--hue), calc(var(--sat) * 50%), 25%);
|
||||
// -> actor panel
|
||||
$bg-inv4: hsl(var(--hue), calc(var(--sat) * 45%), 40%);
|
||||
// -> actor summary
|
||||
/***************/
|
||||
/* actor panel */
|
||||
/***************/
|
||||
|
||||
$fg-inv0: hsl(var(--hue), calc(var(--sat) * 0%), 100%);
|
||||
// -> misc highlighted text; mobile menu icons
|
||||
$fg-inv1: hsl(var(--hue), calc(var(--sat) * 80%), 80%);
|
||||
// -> actor, mobile menu
|
||||
$actor-bg0: hsl($hue, 37.5%, 11%); // bookmarks
|
||||
$actor-bg1: hsl($hue, 32.5%, 15%); // favorites
|
||||
$actor-bg2: hsl($hue, 25%, 20%); // actor raw data; tabs bg
|
||||
$actor-bg3: hsl($hue, 25%, 25%); // actor panel
|
||||
$actor-bg4: hsl($hue, 22.5%, 40%); // actor summary
|
||||
|
||||
$actor-fg0: #fff;
|
||||
$actor-fg1: hsl($hue, 40%, 80%); // actor texts
|
||||
$actor-tabs-bg: hsl($hue, 12%, 17%);
|
||||
|
||||
// OKLCH variants (multiple archives opened)
|
||||
$actor-bg0-ok: oklch(21% 18% var(--actor-hue)); // bookmarks; tabs bg
|
||||
$actor-bg1-ok: oklch(24% 16% var(--actor-hue)); // favorites
|
||||
$actor-bg2-ok: oklch(27% 14% var(--actor-hue)); // actor raw data
|
||||
$actor-bg3-ok: oklch(30% 12% var(--actor-hue)); // actor panel
|
||||
$actor-bg4-ok: oklch(40% 10% var(--actor-hue)); // actor summary
|
||||
$actor-fg1-ok: oklch(92% 8% var(--actor-hue)); // actor texts
|
||||
|
||||
$actor-accent: hsl($hue, 75%, 50%);
|
||||
$actor-accent-ok: oklch(80% 25% var(--actor-hue));
|
||||
$actor-accent2-ok: oklch(60% 20% var(--actor-hue));
|
||||
// -> outline; color code on post
|
||||
|
||||
/***************/
|
||||
/* mobile menu */
|
||||
/***************/
|
||||
|
||||
$menu-bg: hsl($hue, 25%, 20%); // actor-bg2
|
||||
$menu-fg: hsl($hue, 40%, 80%); // actor-fg1
|
||||
$menu-fg-active: #fff;
|
||||
$menu-icon: #fff;
|
||||
$menu-filter-active: hsl($hue, 75%, 50%); // accent-light2
|
||||
|
||||
$menu-backdrop: rgba(0, 0, 0, 0.5);
|
||||
|
||||
$panel-close: hsl($hue, 10%, 20%); // fg1
|
||||
$panel-close-hover: hsl($hue, 75%, 38%); // accent-light
|
||||
|
||||
/**********/
|
||||
/* accent */
|
||||
/**********/
|
||||
|
||||
$accent: hsl(var(--hue), calc(var(--sat) * 200%), 30%);
|
||||
$accent: hsl($hue, 100%, 30%);
|
||||
// -> links, active elements, counters, misc background stripes (color 1)
|
||||
$accent-dark: hsl(var(--hue), calc(var(--sat) * 250%), 26%);
|
||||
$accent-dark: hsl($hue, 100%, 26%);
|
||||
// -> misc background stripes (color 2)
|
||||
$accent-light: hsl(var(--hue), calc(var(--sat) * 150%), 38%);
|
||||
$accent-light: hsl($hue, 75%, 38%);
|
||||
// -> focusable elements outline; main header counter
|
||||
$accent-light2: hsl(var(--hue), calc(var(--sat) * 150%), 50%);
|
||||
$accent-light2: hsl($hue, 75%, 50%);
|
||||
// -> overlay hover/focus icons (SVG); private posts border; active filter indicator (mobile menu)
|
||||
$accent-light3: hsl(var(--hue), calc(var(--sat) * 150%), 75%);
|
||||
// === oklch(56.48% 0.124 59.17)
|
||||
$accent-light3: hsl($hue, 75%, 75%);
|
||||
// -> hover effect on images
|
||||
$accent-light-bg: hsl(var(--hue), calc(var(--sat) * 100%), 96%);
|
||||
$accent-light-bg: hsl($hue, 50%, 96%);
|
||||
// -> private posts background
|
||||
|
||||
/***********/
|
||||
@ -68,7 +84,6 @@ $accent-light-bg: hsl(var(--hue), calc(var(--sat) * 100%), 96%);
|
||||
$overlay-icon: #fff;
|
||||
$overlay-icon-hover: #fff;
|
||||
$overlay-backdrop: rgba(0, 0, 0, 0.75);
|
||||
$menu-backdrop: rgba(0, 0, 0, 0.5);
|
||||
|
||||
/*****************/
|
||||
/* form elements */
|
||||
@ -79,11 +94,11 @@ $bg-input-hover: rgba(255, 255, 255, 0.65);
|
||||
$bg-input-focus: rgba(255, 255, 255, 1);
|
||||
|
||||
$bg-button: rgba(255, 255, 255, 1);
|
||||
$bg-button-hover: hsl(var(--hue), calc(var(--sat) * 100%), 95%);
|
||||
$bg-button-hover: hsl($hue, 50%, 95%);
|
||||
$button-svg: $fg1;
|
||||
$button-svg-hover: hsl(var(--hue), calc(var(--sat) * 150%), 50%);
|
||||
$button-svg-focus: hsl(var(--hue), calc(var(--sat) * 150%), 50%);
|
||||
$button-svg-active: hsl(var(--hue), calc(var(--sat) * 150%), 50%);
|
||||
$button-svg-hover: hsl($hue, 75%, 50%);
|
||||
$button-svg-focus: hsl($hue, 75%, 50%);
|
||||
$button-svg-active: hsl($hue, 75%, 50%);
|
||||
|
||||
$fg-button-focus: $accent;
|
||||
$fg-button-active: $accent;
|
||||
@ -91,9 +106,6 @@ $fg-button-active: $accent;
|
||||
// Note: some rgba values hardcoded, used mostly for box-shadow or backdrops
|
||||
|
||||
html {
|
||||
--hue: #{$hue};
|
||||
--sat: #{$sat};
|
||||
|
||||
--bg0: #{$bg0};
|
||||
--bg1: #{$bg1};
|
||||
--bg2: #{$bg2};
|
||||
@ -103,15 +115,15 @@ html {
|
||||
--fg0: #{$fg0};
|
||||
--fg1: #{$fg1};
|
||||
--fg2: #{$fg2};
|
||||
--fg-inv: #{$fg-inv};
|
||||
|
||||
--bg-inv0: #{$bg-inv0};
|
||||
--bg-inv1: #{$bg-inv1};
|
||||
--bg-inv2: #{$bg-inv2};
|
||||
--bg-inv3: #{$bg-inv3};
|
||||
--bg-inv4: #{$bg-inv4};
|
||||
|
||||
--fg-inv0: #{$fg-inv0};
|
||||
--fg-inv1: #{$fg-inv1};
|
||||
--menu-bg: #{$menu-bg};
|
||||
--menu-fg: #{$menu-fg};
|
||||
--menu-fg-active: #{$menu-fg-active};
|
||||
--menu-icon: #{$menu-icon};
|
||||
--menu-filter-active: #{$menu-filter-active};
|
||||
--panel-close: #{$panel-close};
|
||||
--panel-close-hover: #{$panel-close-hover};
|
||||
|
||||
--accent: #{$accent};
|
||||
--accent-dark: #{$accent-dark};
|
||||
@ -139,3 +151,33 @@ html {
|
||||
--fg-button-focus: #{$fg-button-focus};
|
||||
--fg-button-active: #{$fg-button-active};
|
||||
}
|
||||
|
||||
.actors-wrapper {
|
||||
--actor-hue: 0;
|
||||
--actor-bg0: #{$actor-bg0};
|
||||
--actor-bg0-ok: #{$actor-bg0-ok};
|
||||
--actor-bg1: #{$actor-bg1};
|
||||
--actor-bg1-ok: #{$actor-bg1-ok};
|
||||
--actor-bg2: #{$actor-bg2};
|
||||
--actor-bg2-ok: #{$actor-bg2-ok};
|
||||
--actor-bg3: #{$actor-bg3};
|
||||
--actor-bg3-ok: #{$actor-bg3-ok};
|
||||
--actor-bg4: #{$actor-bg4};
|
||||
--actor-bg4-ok: #{$actor-bg4-ok};
|
||||
|
||||
--actor-fg0: #{$actor-fg0};
|
||||
--actor-fg1: #{$actor-fg1};
|
||||
--actor-fg1-ok: #{$actor-fg1-ok};
|
||||
--actor-tabs-bg: #{$actor-tabs-bg};
|
||||
--actor-accent: #{$actor-accent};
|
||||
--actor-accent-ok: #{$actor-accent-ok};
|
||||
--actor-accent2-ok: #{$actor-accent2-ok};
|
||||
}
|
||||
.actors-tabs button {
|
||||
--actor-accent-ok: #{$actor-accent-ok};
|
||||
--actor-accent2-ok: #{$actor-accent2-ok};
|
||||
}
|
||||
.toot-content {
|
||||
--actor-accent-ok: #{$actor-accent-ok};
|
||||
--actor-accent2-ok: #{$actor-accent2-ok};
|
||||
}
|
||||
|
@ -96,7 +96,7 @@
|
||||
}
|
||||
|
||||
&.active label {
|
||||
color: var(--fg-inv0);
|
||||
color: var(--fg-inv);
|
||||
opacity: 1;
|
||||
background-color: var(--accent);
|
||||
@media (forced-colors: active) {
|
||||
|
@ -211,7 +211,6 @@ details {
|
||||
|
||||
// focusable elements
|
||||
*:focus-visible,
|
||||
// .att-img-wrapper:hover,
|
||||
.tags-group button:focus-visible div,
|
||||
.toots-filter:has([type="checkbox"]:focus-visible) {
|
||||
text-decoration: none;
|
||||
|
@ -6,8 +6,8 @@ $panel-width: 400px;
|
||||
display: none;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
color: var(--fg-inv1);
|
||||
background-color: var(--bg-inv2);
|
||||
color: var(--menu-fg);
|
||||
background-color: var(--menu-bg);
|
||||
|
||||
@media (forced-colors: active) {
|
||||
color: buttonText;
|
||||
@ -53,7 +53,7 @@ $panel-width: 400px;
|
||||
width: $icon-width;
|
||||
height: auto;
|
||||
margin: 5px auto 0;
|
||||
fill: var(--fg-inv0);
|
||||
fill: var(--menu-icon);
|
||||
|
||||
@media (forced-colors: active) {
|
||||
fill: buttonText;
|
||||
@ -79,7 +79,7 @@ $panel-width: 400px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 10px;
|
||||
background-color: var(--accent-light2);
|
||||
background-color: var(--menu-filter-active);
|
||||
@media (forced-colors: active) {
|
||||
background-color: Highlight;
|
||||
}
|
||||
@ -153,6 +153,10 @@ $panel-width: 400px;
|
||||
width: min(#{$panel-width}, calc(100dvw - #{$menu-width}));
|
||||
box-shadow: 0 0 0 0 rgba(0, 0, 0, 0);
|
||||
|
||||
&.actor::before {
|
||||
content: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
transition: left 0.2s ease-out;
|
||||
transition-property: left, box-shadow;
|
||||
@ -164,7 +168,7 @@ $panel-width: 400px;
|
||||
right: 6px;
|
||||
top: 6px;
|
||||
z-index: 1;
|
||||
color: var(--fg1);
|
||||
color: var(--panel-close);
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
.btn-icon {
|
||||
@ -173,12 +177,16 @@ $panel-width: 400px;
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:focus-visible {
|
||||
color: var(--accent-light);
|
||||
color: var(--panel-close-hover);
|
||||
}
|
||||
|
||||
@media (forced-colors: active) {
|
||||
color: buttonText;
|
||||
}
|
||||
|
||||
.actor & {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-menu {
|
||||
@ -205,7 +213,7 @@ $panel-width: 400px;
|
||||
&.menu-open-actor .menu-actor,
|
||||
&.menu-open-filters .menu-filters,
|
||||
&.menu-open-tags .menu-tags {
|
||||
color: var(--fg-inv0);
|
||||
color: var(--menu-fg-active);
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
}
|
||||
|
@ -82,10 +82,10 @@ $display-wide: 340px;
|
||||
}
|
||||
}
|
||||
.active button div {
|
||||
color: var(--fg-inv0);
|
||||
color: var(--fg-inv);
|
||||
background-color: var(--accent);
|
||||
.count {
|
||||
color: var(--fg-inv0);
|
||||
color: var(--fg-inv);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -37,7 +37,7 @@ $meta-visible: 100ch;
|
||||
.toot-summary {
|
||||
padding: 0.5rem 1rem;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--fg-inv0);
|
||||
color: var(--fg-inv);
|
||||
background: repeating-linear-gradient(
|
||||
45deg,
|
||||
var(--accent-dark),
|
||||
@ -98,6 +98,23 @@ $meta-visible: 100ch;
|
||||
}
|
||||
}
|
||||
|
||||
.multiple-actors .toot-content {
|
||||
position: relative;
|
||||
padding-left: 1.6rem;
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0.35rem;
|
||||
top: 0.35rem;
|
||||
bottom: 0.35rem;
|
||||
z-index: 1;
|
||||
width: 0.25rem;
|
||||
border-radius: 0.25rem;
|
||||
background-color: red;
|
||||
background-color: var(--actor-accent-ok, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
.toot-attachments {
|
||||
margin: 1rem 0 0;
|
||||
ul,
|
||||
|
@ -66,7 +66,7 @@
|
||||
align-items: center;
|
||||
font-size: 2em;
|
||||
background-color: var(--accent);
|
||||
color: var(--fg-inv0);
|
||||
color: var(--fg-inv);
|
||||
background: repeating-linear-gradient(
|
||||
45deg,
|
||||
var(--accent-dark),
|
||||
@ -128,3 +128,10 @@
|
||||
background: var(--bg2);
|
||||
}
|
||||
}
|
||||
|
||||
.loading-more {
|
||||
justify-content: center;
|
||||
.file-loader {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
522
index.html
522
index.html
@ -7,7 +7,7 @@
|
||||
<link rel="icon" href="data:," />
|
||||
<base target="_blank" />
|
||||
|
||||
<link rel="stylesheet" href="css/main.css?20241201" />
|
||||
<link rel="stylesheet" href="css/main.css?20241209" />
|
||||
<script>
|
||||
const isFileProtocol = window.location.protocol === "file:";
|
||||
const scripts = [
|
||||
@ -52,9 +52,9 @@
|
||||
id="app"
|
||||
x-data
|
||||
:class="$store.ui.appClasses"
|
||||
x-init="$watch('$store.files.allLoaded', value => checkAllLoaded(value))"
|
||||
x-init="$watch('$store.files.appReady', value => checkAppReady(value))"
|
||||
>
|
||||
<template x-if="! $store.files.allLoaded">
|
||||
<template x-if="!$store.files.appReady && !$store.files.someFilesLoaded">
|
||||
<main
|
||||
class="welcome"
|
||||
:class="$store.files.loading ? 'file-loading' : ''"
|
||||
@ -91,6 +91,7 @@
|
||||
class="file-loader"
|
||||
type="file"
|
||||
accept=".zip"
|
||||
multiple
|
||||
x-show="!$store.files.loading"
|
||||
@change="unZip(Object.values($event.target.files))"
|
||||
/>
|
||||
@ -105,14 +106,28 @@
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<template x-if="$store.files.allLoaded">
|
||||
<template x-if="!$store.files.appReady && $store.files.someFilesLoaded">
|
||||
<main class="welcome loading-more">
|
||||
<div class="intro">
|
||||
<div class="file-loader loading">
|
||||
<span>... Loading ...</span>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<template x-if="$store.files.appReady">
|
||||
<main
|
||||
class="main-section"
|
||||
id="main-section"
|
||||
tabindex="-1"
|
||||
x-on:resize.window="$store.ui.checkMenuState()"
|
||||
>
|
||||
<div class="main-section-inner" id="main-section-inner">
|
||||
<div
|
||||
class="main-section-inner"
|
||||
id="main-section-inner"
|
||||
:class="$store.files.sources.length > 1 ? 'multiple-actors' : ''"
|
||||
>
|
||||
<div class="mobile-menu" id="mobile-menu">
|
||||
<nav>
|
||||
<ul>
|
||||
@ -185,209 +200,250 @@
|
||||
</button>
|
||||
|
||||
<h2 class="visually-hidden" id="actor-title">Account info</h2>
|
||||
<h3 class="visually-hidden" id="actor-tabs-title">Accounts</h3>
|
||||
|
||||
<div class="actor-pretty">
|
||||
<div class="actor-banner">
|
||||
<template x-if="$store.files.header.noImg">
|
||||
<img alt="header" src="img/no-header.png" />
|
||||
</template>
|
||||
<template x-if="! $store.files.header.noImg">
|
||||
<button
|
||||
id="actor-header"
|
||||
@click="$store.lightbox.openProfileImg('header', 'actor-header')"
|
||||
>
|
||||
<img
|
||||
alt="header"
|
||||
:src="`data:${$store.files.header.type}; base64,${$store.files.header.content}`"
|
||||
/>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="actor-id">
|
||||
<template x-if="$store.files.avatar.noImg">
|
||||
<div class="actor-avatar no-avatar">
|
||||
<img
|
||||
:alt="$store.files.actor.name"
|
||||
src="img/no-avatar.png"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="! $store.files.avatar.noImg">
|
||||
<button
|
||||
id="actor-avatar"
|
||||
class="actor-avatar"
|
||||
@click="$store.lightbox.openProfileImg('avatar', 'actor-avatar')"
|
||||
>
|
||||
<img
|
||||
:alt="$store.files.actor.name"
|
||||
class="actor-avatar"
|
||||
:src="`data:${$store.files.avatar.type}; base64,${$store.files.avatar.content}`"
|
||||
/>
|
||||
</button>
|
||||
</template>
|
||||
<h1
|
||||
class="actor-name"
|
||||
x-text="await $store.files.actor.name"
|
||||
></h1>
|
||||
<div class="actor-url">
|
||||
<a
|
||||
:href="await $store.files.actor.url"
|
||||
x-text="await $store.files.actor.url"
|
||||
></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="actor-summary"
|
||||
x-show="$store.files.actor.summary"
|
||||
x-html="await $store.files.actor.summary"
|
||||
></div>
|
||||
|
||||
<div class="actor-infos">
|
||||
<dl>
|
||||
<dt>Member since</dt>
|
||||
<dd
|
||||
x-text="await formatDate($store.files.actor.published)"
|
||||
></dd>
|
||||
</dl>
|
||||
<template x-for="item in await $store.files.actor.attachment">
|
||||
<dl>
|
||||
<dt x-text="item.name"></dt>
|
||||
<dd x-html="item.value"></dd>
|
||||
</dl>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="actor-posts-count">
|
||||
<div class="total">
|
||||
<span
|
||||
class="count"
|
||||
x-text="formatNumber($store.files.outbox.totalItems)"
|
||||
></span>
|
||||
<span class="label">posts</span>
|
||||
</div>
|
||||
|
||||
<div class="archive">
|
||||
<span
|
||||
class="count"
|
||||
x-text="formatNumber($store.files.toots.length)"
|
||||
></span>
|
||||
<span class="label">in archive</span>
|
||||
</div>
|
||||
|
||||
<details
|
||||
class="comment"
|
||||
x-show="$store.files.toots.length != $store.files.outbox.totalItems"
|
||||
<div
|
||||
class="actors-wrapper"
|
||||
:style="'--actor-hue: '+ $store.files.sources[$store.ui.actorPanel].hue"
|
||||
>
|
||||
<template x-if="$store.files.sources.length > 1">
|
||||
<div
|
||||
class="actors-tabs"
|
||||
role="tablist"
|
||||
aria-labelledby="actor-tabs-title"
|
||||
>
|
||||
<summary>
|
||||
<span class="summary-icon">
|
||||
<svg aria-hidden="true">
|
||||
<use href="#question" />
|
||||
</svg>
|
||||
</span>
|
||||
<span class="summary-label"
|
||||
>Why are those two numbers different?</span
|
||||
<template x-for="(_, source) in $store.files.sources">
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
:id="'actortab-' + source"
|
||||
:aria-selected="source === $store.ui.actorPanel ? 'true': 'false'"
|
||||
:aria-controls="'actorpanel-' + source"
|
||||
:style="'--actor-hue: '+ $store.files.sources[source].hue"
|
||||
@click="$store.ui.openActorPanel(source)"
|
||||
@keyup.right="$store.ui.switchActorPanel('up')"
|
||||
@keyup.left="$store.ui.switchActorPanel('down')"
|
||||
>
|
||||
</summary>
|
||||
<div class="details-content">
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
x-text="$store.files.sources[source].actor.name"
|
||||
></span>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="$store.files.sources.length === 1">
|
||||
<span
|
||||
class="visually-hidden"
|
||||
id="actortab-0"
|
||||
x-text="$store.files.sources[0].actor.name"
|
||||
></span>
|
||||
</template>
|
||||
|
||||
<div class="actor-raw">
|
||||
<div class="actor-raw-inner">
|
||||
<details>
|
||||
<summary>
|
||||
<span class="summary-icon">
|
||||
<svg aria-hidden="true">
|
||||
<use href="#json" />
|
||||
</svg>
|
||||
</span>
|
||||
<span class="summary-label"
|
||||
>Raw data <em>(actor.json)</em></span
|
||||
>
|
||||
</summary>
|
||||
<div class="details-content">
|
||||
<pre x-text="formatJson(await $store.files.actor)"></pre>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
<template x-for="source in $store.files.sources">
|
||||
<div
|
||||
class="actor-panel"
|
||||
:id="'actorpanel-' + source.id"
|
||||
role="tabpanel"
|
||||
tabindex="0"
|
||||
:aria-labelledby="'actortab-' + source.id"
|
||||
x-data="{a: source.actor}"
|
||||
x-show="$store.ui.actorPanel === source.id"
|
||||
>
|
||||
<div class="actor-pretty">
|
||||
<div class="actor-banner">
|
||||
<template x-if="source.header.noImg">
|
||||
<img alt="header" src="img/no-header.png" />
|
||||
</template>
|
||||
<template x-if="! source.header.noImg">
|
||||
<button
|
||||
id="actor-header"
|
||||
@click="$store.lightbox.openProfileImg('header', 'actor-header', source.id)"
|
||||
>
|
||||
<img
|
||||
alt="header"
|
||||
:src="`data:${source.header.type}; base64,${source.header.content}`"
|
||||
/>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="actor-likes-bookmarks">
|
||||
<div class="actor-likes">
|
||||
<details>
|
||||
<summary>
|
||||
<span class="summary-icon">
|
||||
<svg aria-hidden="true">
|
||||
<use href="#favorite" />
|
||||
</svg>
|
||||
</span>
|
||||
<h2 class="summary-label">
|
||||
Favorites (<span
|
||||
class="count"
|
||||
x-text="$store.files.likes.length"
|
||||
></span
|
||||
>)
|
||||
</h2>
|
||||
</summary>
|
||||
<div class="details-content">
|
||||
<template x-if="$store.files.likes.length">
|
||||
<ul>
|
||||
<template x-for="url in $store.files.likes">
|
||||
<li>
|
||||
<a :href="url" x-text="url"></a>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</template>
|
||||
<template x-if="! $store.files.likes.length">
|
||||
<p class="no-content">... no favorites ...</p>
|
||||
</template>
|
||||
<div class="actor-id">
|
||||
<template x-if="source.avatar.noImg">
|
||||
<div class="actor-avatar no-avatar">
|
||||
<img :alt="a.name" src="img/no-avatar.png" />
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="! source.avatar.noImg">
|
||||
<button
|
||||
id="actor-avatar"
|
||||
class="actor-avatar"
|
||||
@click="$store.lightbox.openProfileImg('avatar', 'actor-avatar', source.id)"
|
||||
>
|
||||
<img
|
||||
:alt="a.name"
|
||||
class="actor-avatar"
|
||||
:src="`data:${source.avatar.type}; base64,${source.avatar.content}`"
|
||||
/>
|
||||
</button>
|
||||
</template>
|
||||
<h1 class="actor-name" x-text="await a.name"></h1>
|
||||
<div class="actor-url">
|
||||
<a :href="await a.url" x-text="await a.url"></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="actor-summary"
|
||||
x-show="a.summary"
|
||||
x-html="await a.summary"
|
||||
></div>
|
||||
|
||||
<div class="actor-infos">
|
||||
<dl>
|
||||
<dt>Member since</dt>
|
||||
<dd x-text="await formatDate(a.published)"></dd>
|
||||
</dl>
|
||||
<template x-for="item in await a.attachment">
|
||||
<dl>
|
||||
<dt x-text="item.name"></dt>
|
||||
<dd x-html="item.value"></dd>
|
||||
</dl>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="actor-posts-count">
|
||||
<div class="total">
|
||||
<span
|
||||
class="count"
|
||||
x-text="formatNumber(source.outbox.totalItems)"
|
||||
></span>
|
||||
<span class="label">posts</span>
|
||||
</div>
|
||||
|
||||
<div class="archive">
|
||||
<span
|
||||
class="count"
|
||||
x-text="formatNumber(source.nbToots)"
|
||||
></span>
|
||||
<span class="label">in archive</span>
|
||||
</div>
|
||||
|
||||
<details
|
||||
class="comment"
|
||||
x-show="source.nbToots != source.outbox.totalItems"
|
||||
>
|
||||
<summary>
|
||||
<span class="summary-icon">
|
||||
<svg aria-hidden="true">
|
||||
<use href="#question" />
|
||||
</svg>
|
||||
</span>
|
||||
<span class="summary-label"
|
||||
>Why are those two numbers different?</span
|
||||
>
|
||||
</summary>
|
||||
<div class="details-content">
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
<div class="actor-bookmarks">
|
||||
<details>
|
||||
<summary>
|
||||
<span class="summary-icon">
|
||||
<svg aria-hidden="true">
|
||||
<use href="#bookmark" />
|
||||
</svg>
|
||||
</span>
|
||||
<h2 class="summary-label">
|
||||
Bookmarks (<span
|
||||
class="count"
|
||||
x-text="$store.files.bookmarks.length"
|
||||
></span
|
||||
>)
|
||||
</h2>
|
||||
</summary>
|
||||
<div class="details-content">
|
||||
<template x-if="$store.files.bookmarks.length">
|
||||
<ul>
|
||||
<template x-for="url in $store.files.bookmarks">
|
||||
<li>
|
||||
<a :href="url" x-text="url"></a>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</template>
|
||||
<template x-if="! $store.files.bookmarks.length">
|
||||
<p class="no-content">... no bookmarks ...</p>
|
||||
</template>
|
||||
|
||||
<div class="actor-raw">
|
||||
<div class="actor-raw-inner">
|
||||
<details>
|
||||
<summary>
|
||||
<span class="summary-icon">
|
||||
<svg aria-hidden="true">
|
||||
<use href="#json" />
|
||||
</svg>
|
||||
</span>
|
||||
<span class="summary-label"
|
||||
>Raw data <em>(actor.json)</em></span
|
||||
>
|
||||
</summary>
|
||||
<div class="details-content">
|
||||
<pre x-text="formatJson(await a)"></pre>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<div class="actor-likes-bookmarks">
|
||||
<div class="actor-likes">
|
||||
<details>
|
||||
<summary>
|
||||
<span class="summary-icon">
|
||||
<svg aria-hidden="true">
|
||||
<use href="#favorite" />
|
||||
</svg>
|
||||
</span>
|
||||
<h2 class="summary-label">
|
||||
Favorites (<span
|
||||
class="count"
|
||||
x-text="source.likes.length"
|
||||
></span
|
||||
>)
|
||||
</h2>
|
||||
</summary>
|
||||
<div class="details-content">
|
||||
<template x-if="source.likes.length">
|
||||
<ul>
|
||||
<template x-for="url in source.likes">
|
||||
<li>
|
||||
<a :href="url" x-text="url"></a>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</template>
|
||||
<template x-if="! source.likes.length">
|
||||
<p class="no-content">... no favorites ...</p>
|
||||
</template>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
<div class="actor-bookmarks">
|
||||
<details>
|
||||
<summary>
|
||||
<span class="summary-icon">
|
||||
<svg aria-hidden="true">
|
||||
<use href="#bookmark" />
|
||||
</svg>
|
||||
</span>
|
||||
<h2 class="summary-label">
|
||||
Bookmarks (<span
|
||||
class="count"
|
||||
x-text="source.bookmarks.length"
|
||||
></span
|
||||
>)
|
||||
</h2>
|
||||
</summary>
|
||||
<div class="details-content">
|
||||
<template x-if="source.bookmarks.length">
|
||||
<ul>
|
||||
<template x-for="url in source.bookmarks">
|
||||
<li>
|
||||
<a :href="url" x-text="url"></a>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</template>
|
||||
<template x-if="! source.bookmarks.length">
|
||||
<p class="no-content">... no bookmarks ...</p>
|
||||
</template>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -775,18 +831,21 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="toots-filters-group">
|
||||
<div
|
||||
class="toots-filters-group"
|
||||
x-show="$store.files.toots.length"
|
||||
>
|
||||
<h3 class="toots-filters-group-title">Language</h3>
|
||||
<template x-for="(nb, lang) in $store.files.languages">
|
||||
<div
|
||||
class="toots-filter checkbox"
|
||||
:class="isFilterActive('langs_' + lang) ? 'active' : ''"
|
||||
:class="isFilterActive('lang_' + lang) ? 'active' : ''"
|
||||
>
|
||||
<label :for="'filter-lang-' + lang">
|
||||
<input
|
||||
:id="'filter-lang-' + lang"
|
||||
type="checkbox"
|
||||
x-model="$store.files.filters['langs_' + lang]"
|
||||
x-model="$store.files.filters['lang_' + lang]"
|
||||
@change="$store.files.setFilter()"
|
||||
/>
|
||||
<span>
|
||||
@ -798,6 +857,30 @@
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<template x-if="$store.files.sources.length > 1">
|
||||
<div class="toots-filters-group">
|
||||
<h3 class="toots-filters-group-title">Author</h3>
|
||||
<template x-for="source in $store.files.sources">
|
||||
<div
|
||||
class="toots-filter checkbox"
|
||||
:class="isFilterActive('actor_' + source.id) ? 'active' : ''"
|
||||
>
|
||||
<label :for="'filter-actor-' + source.id">
|
||||
<input
|
||||
:id="'filter-actor-' + source.id"
|
||||
type="checkbox"
|
||||
x-model="$store.files.filters['actor_' + source.id]"
|
||||
@change="$store.files.setFilter()"
|
||||
/>
|
||||
<span>
|
||||
<span x-text="source.actor.name"></span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="toots-filters-reset">
|
||||
<button
|
||||
@click="$store.files.resetFilters(true)"
|
||||
@ -958,6 +1041,7 @@
|
||||
<div
|
||||
class="toot"
|
||||
:class="['toot-type-' + contentType(toot.type).toLowerCase(), 'toot-visibility-' + toot._marl.visibility[0]]"
|
||||
:style="'--actor-hue: '+ $store.files.sources[toot._marl.source].hue"
|
||||
>
|
||||
<h3 class="toot-header visually-hidden">
|
||||
<span x-text="contentType(toot.type)"></span>
|
||||
@ -986,21 +1070,21 @@
|
||||
>
|
||||
<li
|
||||
:class="attachmentWrapperClass(att)"
|
||||
:data-trigger="loadAttachedMedia(att)"
|
||||
:data-trigger="loadAttachedMedia(att, toot._marl.source)"
|
||||
>
|
||||
<!-- image -->
|
||||
<template
|
||||
x-if="attachmentIsImage(att) && $store.files[att.url]"
|
||||
x-if="attachmentIsImage(att) && $store.files.sources[toot._marl.source][att.url]"
|
||||
>
|
||||
<div class="att-wrapper">
|
||||
<button
|
||||
class="att-img-wrapper"
|
||||
@click="$store.lightbox.open(toot.object.attachment, index, toot._marl.id + '-' + index)"
|
||||
@click="$store.lightbox.open(toot, index, toot._marl.id + '-' + index)"
|
||||
:id="toot._marl.id + '-' + index"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
:src="`data:${att.mediaType}; base64,${await $store.files[att.url].content}`"
|
||||
:src="`data:${att.mediaType}; base64,${await $store.files.sources[toot._marl.source][att.url].content}`"
|
||||
:aria-labelledby="toot._marl.id + '-' + index + '-desc'"
|
||||
/>
|
||||
</button>
|
||||
@ -1023,12 +1107,12 @@
|
||||
|
||||
<!-- sound -->
|
||||
<template
|
||||
x-if="attachmentIsSound(att) && $store.files[att.url]"
|
||||
x-if="attachmentIsSound(att) && $store.files.sources[toot._marl.source][att.url]"
|
||||
>
|
||||
<div class="att-wrapper">
|
||||
<audio
|
||||
controls
|
||||
:src="`data:${att.mediaType}; base64,${await $store.files[att.url].content}`"
|
||||
:src="`data:${att.mediaType}; base64,${await $store.files.sources[toot._marl.source][att.url].content}`"
|
||||
:aria-labelledby="toot._marl.id + '-' + index + '-desc'"
|
||||
></audio>
|
||||
<div
|
||||
@ -1049,14 +1133,14 @@
|
||||
|
||||
<!-- video -->
|
||||
<template
|
||||
x-if="attachmentIsVideo(att) && $store.files[att.url]"
|
||||
x-if="attachmentIsVideo(att) && $store.files.sources[toot._marl.source][att.url]"
|
||||
>
|
||||
<div class="att-wrapper">
|
||||
<video
|
||||
controls
|
||||
:width="att.width"
|
||||
:height="att.height"
|
||||
:src="`data:${att.mediaType}; base64,${await $store.files[att.url].content}`"
|
||||
:src="`data:${att.mediaType}; base64,${await $store.files.sources[toot._marl.source][att.url].content}`"
|
||||
:aria-labelledby="toot._marl.id + '-' + index + '-desc'"
|
||||
></video>
|
||||
<div
|
||||
@ -1195,10 +1279,16 @@
|
||||
<use href="#json" />
|
||||
</svg>
|
||||
</span>
|
||||
<span class="summary-label">Raw data</span>
|
||||
<span class="summary-label" :id="'raw-' + toot._marl.id"
|
||||
>Raw data</span
|
||||
>
|
||||
</summary>
|
||||
<div class="details-content">
|
||||
<textarea x-text="formatJson(toot)" readonly></textarea>
|
||||
<textarea
|
||||
x-text="formatJson(toot)"
|
||||
:aria-labelledby="'raw-' + toot._marl.id"
|
||||
readonly
|
||||
></textarea>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
@ -1454,7 +1544,7 @@
|
||||
<div class="overlay-content">
|
||||
<img
|
||||
:alt="$store.lightbox.data[$store.lightbox.index].name"
|
||||
:src="`data:${$store.lightbox.data[$store.lightbox.index].mediaType}; base64,${$store.files[$store.lightbox.data[$store.lightbox.index].url].content}`"
|
||||
:src="`data:${$store.lightbox.data[$store.lightbox.index].mediaType}; base64,${$store.files.sources[$store.lightbox.source][$store.lightbox.data[$store.lightbox.index].url].content}`"
|
||||
@click="$store.lightbox.showNext()"
|
||||
/>
|
||||
</div>
|
||||
@ -1596,6 +1686,6 @@
|
||||
</symbol>
|
||||
</svg>
|
||||
|
||||
<script src="js/main.js?20241202"></script>
|
||||
<script src="js/main.js?20241209"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
532
js/main.js
532
js/main.js
@ -37,31 +37,18 @@ const userPrefsStore = {
|
||||
|
||||
const filesStore = {
|
||||
resetState() {
|
||||
this.raw = {};
|
||||
this.actor = {};
|
||||
this.outbox = {};
|
||||
this.sources = [];
|
||||
this.toots = [];
|
||||
this.likes = [];
|
||||
this.bookmarks = [];
|
||||
this.avatar = {};
|
||||
this.header = {};
|
||||
|
||||
this.sortAsc = true; // -> userPrefs
|
||||
this.pageSize = 10; // -> userPrefs
|
||||
this.currentPage = 1;
|
||||
|
||||
this.loading = false;
|
||||
this.loaded = {
|
||||
actor: false,
|
||||
avatar: false,
|
||||
banner: false,
|
||||
outbox: false,
|
||||
likes: false,
|
||||
bookmarks: false,
|
||||
};
|
||||
this.someFilesLoaded = false;
|
||||
|
||||
this.languages = {};
|
||||
this.boostsAuthors = {};
|
||||
this.boostsAuthors = [];
|
||||
|
||||
this.filters = {};
|
||||
this.filtersDefault = {
|
||||
@ -91,9 +78,13 @@ const filesStore = {
|
||||
attachmentWithAltText: false,
|
||||
|
||||
// automatically generated (see loadJsonFile()):
|
||||
// langs_en: true,
|
||||
// langs_fr: true,
|
||||
// langs_de: true,
|
||||
// lang_en: true,
|
||||
// lang_fr: true,
|
||||
// lang_de: true,
|
||||
// etc
|
||||
// actor_0: true,
|
||||
// actor_1: true,
|
||||
// actor_2: true,
|
||||
// etc
|
||||
};
|
||||
this.filtersActive = false;
|
||||
@ -412,8 +403,8 @@ const filesStore = {
|
||||
}
|
||||
}
|
||||
|
||||
for (let lang in this.languages) {
|
||||
if (f.hasOwnProperty("langs_" + lang) && f["langs_" + lang] === false) {
|
||||
for (const lang in this.languages) {
|
||||
if (f.hasOwnProperty("lang_" + lang) && f["lang_" + lang] === false) {
|
||||
if (t.type === "Create") {
|
||||
if (t.object.contentMap.hasOwnProperty(lang)) {
|
||||
return false;
|
||||
@ -424,6 +415,15 @@ const filesStore = {
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
});
|
||||
},
|
||||
@ -524,15 +524,26 @@ const filesStore = {
|
||||
return r;
|
||||
},
|
||||
|
||||
get allLoaded() {
|
||||
return (
|
||||
this.loaded.actor &&
|
||||
this.loaded.avatar &&
|
||||
this.loaded.banner &&
|
||||
this.loaded.outbox &&
|
||||
this.loaded.likes &&
|
||||
this.loaded.bookmarks
|
||||
);
|
||||
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() {
|
||||
@ -540,7 +551,7 @@ const filesStore = {
|
||||
},
|
||||
get pagedToots() {
|
||||
if (this.filteredToots) {
|
||||
return this.filteredToots.filter((toot, index) => {
|
||||
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;
|
||||
@ -559,7 +570,6 @@ const filesStore = {
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
toggleTootsOrder() {
|
||||
this.sortAsc = !this.sortAsc;
|
||||
this.sortToots();
|
||||
@ -570,10 +580,7 @@ const filesStore = {
|
||||
checkPagingValue() {
|
||||
if (this.currentPage < 1) {
|
||||
this.currentPage = 1;
|
||||
} else if (
|
||||
(this.currentPage - 1) * this.pageSize >
|
||||
this.filteredToots.length
|
||||
) {
|
||||
} else if (this.currentPage > this.totalPages) {
|
||||
this.currentPage = this.totalPages;
|
||||
}
|
||||
},
|
||||
@ -607,38 +614,47 @@ const lightboxStore = {
|
||||
resetState() {
|
||||
this.show = false;
|
||||
this.data = [];
|
||||
this.source = 0;
|
||||
this.index = 0;
|
||||
this.source = "";
|
||||
this.origin = "";
|
||||
},
|
||||
|
||||
open(att, index, source) {
|
||||
this.data = att;
|
||||
open(toot, index, origin) {
|
||||
this.data = toot.object.attachment;
|
||||
this.source = toot._marl.source;
|
||||
this.show = true;
|
||||
this.index = index;
|
||||
this.source = source;
|
||||
this.origin = origin;
|
||||
document.getElementById("main-section-inner").setAttribute("inert", true);
|
||||
setTimeout(() => {
|
||||
document.getElementById("lightbox").focus();
|
||||
}, 50);
|
||||
},
|
||||
openProfileImg(name, source) {
|
||||
const data = [
|
||||
{
|
||||
name: name,
|
||||
url: name,
|
||||
mediaType: Alpine.store("files")[name].type,
|
||||
openProfileImg(name, origin, source) {
|
||||
const data = {
|
||||
object: {
|
||||
attachment: [
|
||||
{
|
||||
name: name,
|
||||
url: name,
|
||||
mediaType: Alpine.store("files").sources[source][name].type,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
this.open(data, 0, source);
|
||||
_marl: {
|
||||
source: source,
|
||||
},
|
||||
};
|
||||
this.open(data, 0, origin);
|
||||
},
|
||||
close() {
|
||||
const source = this.source;
|
||||
const origin = this.origin;
|
||||
this.data = [];
|
||||
this.index = 0;
|
||||
this.show = false;
|
||||
this.source = "";
|
||||
this.origin = "";
|
||||
document.getElementById("main-section-inner").removeAttribute("inert");
|
||||
document.getElementById(source).focus();
|
||||
document.getElementById(origin).focus();
|
||||
},
|
||||
showNext() {
|
||||
this.index++;
|
||||
@ -664,6 +680,7 @@ const uiStore = {
|
||||
resetState() {
|
||||
this.pagingOptionsVisible = false;
|
||||
this.openMenu = "";
|
||||
this.actorPanel = 0;
|
||||
this.menuIsActive = false;
|
||||
},
|
||||
|
||||
@ -680,6 +697,26 @@ const uiStore = {
|
||||
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 = "";
|
||||
@ -773,19 +810,6 @@ const uiStore = {
|
||||
|
||||
// utils
|
||||
|
||||
function unZip(file) {
|
||||
resetStores();
|
||||
Alpine.store("files").loading = true;
|
||||
JSZip.loadAsync(file[0]).then(function (content) {
|
||||
Alpine.store("files").raw = content.files;
|
||||
|
||||
loadJsonFile("actor");
|
||||
loadJsonFile("outbox");
|
||||
loadJsonFile("likes");
|
||||
loadJsonFile("bookmarks");
|
||||
});
|
||||
}
|
||||
|
||||
function resetStores() {
|
||||
Alpine.store("files").resetState();
|
||||
Alpine.store("lightbox").resetState();
|
||||
@ -795,14 +819,78 @@ function resetStores() {
|
||||
Alpine.store("userPrefs").load("pageSize");
|
||||
}
|
||||
|
||||
function loadJsonFile(name) {
|
||||
const content = Alpine.store("files").raw;
|
||||
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
|
||||
);
|
||||
})
|
||||
) {
|
||||
console.warn("File already loaded:", file.name);
|
||||
continue;
|
||||
}
|
||||
|
||||
let index = Alpine.store("files").sources.length;
|
||||
|
||||
Alpine.store("files").sources[index] = {
|
||||
id: index,
|
||||
fileInfos: {
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
lastModified: file.lastModified,
|
||||
},
|
||||
nbToots: 0,
|
||||
|
||||
actor: {},
|
||||
outbox: {},
|
||||
likes: [],
|
||||
bookmarks: [],
|
||||
avatar: {},
|
||||
header: {},
|
||||
|
||||
loaded: {
|
||||
actor: false,
|
||||
avatar: false,
|
||||
header: false,
|
||||
outbox: false,
|
||||
likes: false,
|
||||
bookmarks: false,
|
||||
},
|
||||
};
|
||||
|
||||
JSZip.loadAsync(file).then(function (content) {
|
||||
Alpine.store("files").sources[index]._raw = content.files;
|
||||
|
||||
loadJsonFile("actor", index);
|
||||
loadJsonFile("outbox", index);
|
||||
loadJsonFile("likes", index);
|
||||
loadJsonFile("bookmarks", index);
|
||||
});
|
||||
}
|
||||
|
||||
setHueForSources();
|
||||
}
|
||||
|
||||
function loadJsonFile(name, index) {
|
||||
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
|
||||
console.warn(`File ${name}.json not found in archive.`);
|
||||
Alpine.store("files").loaded[name] = true;
|
||||
Alpine.store("files").sources[index].loaded[name] = true;
|
||||
} else {
|
||||
// this should NOT happen and will prevent the app from running
|
||||
console.error(`File ${name}.json not found in archive.`);
|
||||
@ -812,114 +900,131 @@ function loadJsonFile(name) {
|
||||
|
||||
content[name + ".json"].async("text").then(function (txt) {
|
||||
if (name === "actor") {
|
||||
Alpine.store("files").actor = JSON.parse(txt);
|
||||
loadActorImages();
|
||||
Alpine.store("files").loaded.actor = true;
|
||||
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.map(preprocessToots);
|
||||
Alpine.store("files").toots = toots;
|
||||
|
||||
let infos = toots.reduce(
|
||||
(accu, toot) => {
|
||||
if (toot.type === "Create") {
|
||||
const map = toot.object.contentMap;
|
||||
for (let lang in map) {
|
||||
if (!accu.langs[lang]) {
|
||||
accu.langs[lang] = 1;
|
||||
} else {
|
||||
accu.langs[lang]++;
|
||||
}
|
||||
}
|
||||
} else 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: {} }
|
||||
);
|
||||
|
||||
let boosts = [];
|
||||
for (var key in infos.boosts) {
|
||||
boosts.push(infos.boosts[key]);
|
||||
}
|
||||
|
||||
Alpine.store("files").languages = infos.langs;
|
||||
Alpine.store("files").boostsAuthors = boosts;
|
||||
|
||||
for (let lang in infos.langs) {
|
||||
Alpine.store("files").filtersDefault["langs_" + lang] = true;
|
||||
}
|
||||
|
||||
let toots = data.orderedItems.map((t) => preprocessToots(t, index));
|
||||
Alpine.store("files").toots = Alpine.store("files").toots.concat(toots); // ### duplicate toots?
|
||||
Alpine.store("files").sources[index].nbToots = toots.length;
|
||||
delete data.orderedItems;
|
||||
Alpine.store("files").outbox = data;
|
||||
|
||||
Alpine.store("files").resetFilters(false);
|
||||
Alpine.store("files").loaded.outbox = true;
|
||||
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")[name] = tmp.orderedItems;
|
||||
Alpine.store("files").loaded[name] = true;
|
||||
Alpine.store("files").sources[index][name] = tmp.orderedItems;
|
||||
Alpine.store("files").sources[index].loaded[name] = true;
|
||||
} // likes.json || bookmarks.json
|
||||
});
|
||||
}
|
||||
|
||||
function preprocessToots(t) {
|
||||
let marl = {};
|
||||
function buildTootsInfos() {
|
||||
let langs = {};
|
||||
let boosts = [];
|
||||
|
||||
if (Alpine.store("files").toots.length > 0) {
|
||||
let infos = Alpine.store("files").toots.reduce(
|
||||
(accu, toot) => {
|
||||
if (toot.type === "Create") {
|
||||
const map = toot.object.contentMap;
|
||||
for (let lang in map) {
|
||||
if (!accu.langs[lang]) {
|
||||
accu.langs[lang] = 1;
|
||||
} else {
|
||||
accu.langs[lang]++;
|
||||
}
|
||||
}
|
||||
} else 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) {
|
||||
let marl = {
|
||||
source: index,
|
||||
};
|
||||
|
||||
if (typeof t.object === "object" && t.object !== null) {
|
||||
if (t.object.contentMap) {
|
||||
@ -955,87 +1060,106 @@ function preprocessToots(t) {
|
||||
return t;
|
||||
}
|
||||
|
||||
function loadActorImages() {
|
||||
const actor = Alpine.store("files").actor;
|
||||
const content = Alpine.store("files").raw;
|
||||
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) {
|
||||
const image = actor.icon;
|
||||
content[image.url].async("base64").then(function (content) {
|
||||
Alpine.store("files").avatar = {
|
||||
Alpine.store("files").sources[index].avatar = {
|
||||
type: image.mediaType,
|
||||
content: content,
|
||||
noImg: false,
|
||||
};
|
||||
Alpine.store("files").loaded.avatar = true;
|
||||
Alpine.store("files").sources[index].loaded.avatar = true;
|
||||
});
|
||||
} else {
|
||||
Alpine.store("files").avatar = { noImg: true };
|
||||
Alpine.store("files").loaded.avatar = true;
|
||||
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) {
|
||||
const image = actor.image;
|
||||
content[image.url].async("base64").then(function (content) {
|
||||
Alpine.store("files").header = {
|
||||
Alpine.store("files").sources[index].header = {
|
||||
type: image.mediaType,
|
||||
content: content,
|
||||
noImg: false,
|
||||
};
|
||||
Alpine.store("files").loaded.banner = true;
|
||||
Alpine.store("files").sources[index].loaded.header = true;
|
||||
});
|
||||
} else {
|
||||
Alpine.store("files").header = { noImg: true };
|
||||
Alpine.store("files").loaded.banner = true;
|
||||
Alpine.store("files").sources[index].header = { noImg: true };
|
||||
Alpine.store("files").sources[index].loaded.header = true;
|
||||
}
|
||||
}
|
||||
|
||||
function loadAttachedMedia(att) {
|
||||
if (
|
||||
attachmentIsImage(att) ||
|
||||
attachmentIsVideo(att) ||
|
||||
attachmentIsSound(att)
|
||||
) {
|
||||
const data = Alpine.store("files").raw;
|
||||
let url = att.url;
|
||||
if (url.indexOf("/") === 0) {
|
||||
url = url.slice(1);
|
||||
}
|
||||
data[url].async("base64").then((content) => {
|
||||
Alpine.store("files")[att.url] = {
|
||||
type: att.mediaType,
|
||||
content: content,
|
||||
};
|
||||
});
|
||||
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 checkAllLoaded(ok) {
|
||||
function checkAppReady(ok) {
|
||||
if (ok) {
|
||||
buildTootsInfos();
|
||||
buildDynamicFilters();
|
||||
cleanUpRaw();
|
||||
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() {
|
||||
const content = Alpine.store("files").raw;
|
||||
const actor = Alpine.store("files").actor;
|
||||
for (let i = 0; i < Alpine.store("files").sources.length; i++) {
|
||||
const content = Alpine.store("files").sources[i]._raw;
|
||||
if (content.cleanedUp) {
|
||||
continue;
|
||||
}
|
||||
|
||||
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"];
|
||||
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").raw = content;
|
||||
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;
|
||||
if (url.indexOf("/") === 0) {
|
||||
url = url.slice(1);
|
||||
}
|
||||
data[url].async("base64").then((content) => {
|
||||
Alpine.store("files").sources[index][att.url] = {
|
||||
type: att.mediaType,
|
||||
content: content,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function pagingUpdated() {
|
||||
|
16
readme.md
16
readme.md
@ -45,6 +45,10 @@ No need to unpack your archive. Just drag'n'drop your file in the MARL window, a
|
||||
|
||||
Everything takes place in the browser, with no communication with any server (once the page has been loaded). Your archive data is _not_ sent to any server. No analytics are included either.
|
||||
|
||||
### Support for multiple archives
|
||||
|
||||
You can open multiple archive files at once (or add some more after via drag'n'drop) and MARL will show you all their posts in a single chronological list, as well as all the profiles in a tabbed interface. Each profile is automatically color-coded. A new group of filters allow you to filter posts by author.
|
||||
|
||||
### Filters
|
||||
|
||||
Many filters allow you to quickly find a subset of posts in your archive:
|
||||
@ -112,3 +116,15 @@ This is a personal, non-official project. I am not associated with the Mastodon
|
||||
You can reach me via github or on Mastodon:
|
||||
Github: https://github.com/s427
|
||||
Mastodon: https://lou.lt/@s427
|
||||
|
||||
## Version history
|
||||
|
||||
- v. 1.2
|
||||
- [NEW] support for multiple archive files. Notes:
|
||||
- you can select multiple files from the open dialog;
|
||||
- if one or several files are already loaded, you can drag and drop more files anywhere on the app window in order for MARL to load those new files and add them to the ones already loaded;
|
||||
- for now the "Load new file" button still assumes you want to start over (blank slate); this will probably be changed in a future version.
|
||||
- v. 1.1
|
||||
- [NEW] paging preferences (page size and posts order) are automatically saved to the browser and restored on app load if present
|
||||
- various bug fixes and improvements
|
||||
- v. 1.0 - Initial release
|
||||
|
Loading…
x
Reference in New Issue
Block a user