├── .gitignore
├── .nojekyll
├── .prettierignore
├── Dockerfile
├── LICENSE
├── dev
├── README.md
├── astro.config.mjs
├── package-lock.json
├── package.json
├── public
│ └── js
│ │ ├── MARL
│ │ ├── init.js
│ │ ├── libs.js
│ │ ├── loadData.js
│ │ ├── stores.js
│ │ └── utils.js
│ │ ├── config.default.js
│ │ ├── i18n
│ │ ├── _langs.js
│ │ ├── en.js
│ │ ├── es.js
│ │ └── fr.js
│ │ └── libs
│ │ ├── alpinejs-i18n.min.js
│ │ ├── alpinejs.min.js
│ │ └── jszip.min.js
├── src
│ ├── appVersion.js
│ ├── components
│ │ ├── App.astro
│ │ ├── LightBox.astro
│ │ ├── MobileMenu.astro
│ │ ├── PagingBottom.astro
│ │ ├── PagingOptions.astro
│ │ ├── PagingTop.astro
│ │ ├── SvgSprites.astro
│ │ ├── panels
│ │ │ ├── ActorPanel.astro
│ │ │ ├── CloseBtn.astro
│ │ │ ├── ConversationPanel.astro
│ │ │ ├── FiltersPanel.astro
│ │ │ ├── HeaderPanel.astro
│ │ │ ├── PostsPanel.astro
│ │ │ ├── TagsPanel.astro
│ │ │ ├── ToolsPanel.astro
│ │ │ ├── actor
│ │ │ │ ├── ImgAvatar.astro
│ │ │ │ ├── ImgHeader.astro
│ │ │ │ └── LikesBookmarks.astro
│ │ │ ├── filters
│ │ │ │ ├── FilterActivity.astro
│ │ │ │ ├── FilterCheckbox.astro
│ │ │ │ ├── FilterDate.astro
│ │ │ │ ├── FilterText.astro
│ │ │ │ └── FilterTime.astro
│ │ │ ├── posts
│ │ │ │ ├── AttachmentDescription.astro
│ │ │ │ ├── AttachmentImage.astro
│ │ │ │ ├── AttachmentSound.astro
│ │ │ │ ├── AttachmentVideo.astro
│ │ │ │ ├── HasReply.astro
│ │ │ │ ├── Poll.astro
│ │ │ │ ├── Post.astro
│ │ │ │ ├── PostAnnounce.astro
│ │ │ │ ├── PostCreate.astro
│ │ │ │ ├── PostInfos.astro
│ │ │ │ ├── PostRaw.astro
│ │ │ │ └── PostTags.astro
│ │ │ ├── tags
│ │ │ │ ├── TagsGroup.astro
│ │ │ │ ├── TagsGroupHeader.astro
│ │ │ │ └── TagsGroupMain.astro
│ │ │ └── tools
│ │ │ │ ├── About.astro
│ │ │ │ ├── AppLog.astro
│ │ │ │ ├── AppSettings.astro
│ │ │ │ ├── ManageFiles.astro
│ │ │ │ └── RemoteInfos.astro
│ │ └── screens
│ │ │ ├── LoadingScreen.astro
│ │ │ ├── MainScreen.astro
│ │ │ └── WelcomeScreen.astro
│ ├── css
│ │ ├── _actor.scss
│ │ ├── _colors-mixins.scss
│ │ ├── _colors.scss
│ │ ├── _conversation.scss
│ │ ├── _filters.scss
│ │ ├── _forms.scss
│ │ ├── _global.scss
│ │ ├── _layout.scss
│ │ ├── _menu.scss
│ │ ├── _mixins.scss
│ │ ├── _overlay.scss
│ │ ├── _post.scss
│ │ ├── _tags.scss
│ │ ├── _tools.scss
│ │ ├── _welcome.scss
│ │ └── main.scss
│ ├── img
│ │ ├── no-avatar-dark.png
│ │ ├── no-avatar.png
│ │ ├── no-header-dark.png
│ │ └── no-header.png
│ ├── pages
│ │ └── index.astro
│ └── svg
│ │ ├── dark
│ │ ├── activity-like.svg
│ │ ├── activity-share.svg
│ │ ├── at.svg
│ │ ├── calendar.svg
│ │ ├── checkbox-checked.svg
│ │ ├── checkbox-unchecked.svg
│ │ ├── hashtag.svg
│ │ ├── link.svg
│ │ ├── open-in-new.svg
│ │ ├── person.svg
│ │ ├── type-boost.svg
│ │ ├── type-post.svg
│ │ ├── visibility-followers.svg
│ │ ├── visibility-mentioned.svg
│ │ ├── visibility-public.svg
│ │ └── visibility-unlisted.svg
│ │ └── light
│ │ ├── activity-like.svg
│ │ ├── activity-share.svg
│ │ ├── calendar.svg
│ │ ├── checkbox-checked.svg
│ │ ├── checkbox-unchecked.svg
│ │ ├── hashtag.svg
│ │ ├── link.svg
│ │ ├── menu-filters.svg
│ │ ├── menu-new.svg
│ │ ├── menu-profile.svg
│ │ ├── menu-tags.svg
│ │ ├── no-alt-text.svg
│ │ ├── open-in-new.svg
│ │ ├── person.svg
│ │ ├── type-boost.svg
│ │ ├── type-post.svg
│ │ ├── visibility-followers.svg
│ │ ├── visibility-mentioned.svg
│ │ ├── visibility-public.svg
│ │ └── visibility-unlisted.svg
└── tsconfig.json
├── dist
├── _astro
│ ├── index.Bc9gEahN.css
│ ├── no-avatar-dark.DVWNt87i.png
│ ├── no-avatar.DeffBVR2.png
│ ├── no-header-dark.BP11rozj.png
│ └── no-header.DKXzxkmt.png
├── index.html
└── js
│ ├── MARL
│ ├── init.js
│ ├── libs.js
│ ├── loadData.js
│ ├── stores.js
│ └── utils.js
│ ├── config.default.js
│ ├── i18n
│ ├── _langs.js
│ ├── en.js
│ ├── es.js
│ └── fr.js
│ └── libs
│ ├── alpinejs-i18n.min.js
│ ├── alpinejs.min.js
│ └── jszip.min.js
├── index.html
├── readme.md
├── screenshots
├── screenshot-dark-theme.png
├── screenshot-multiple-archives.png
└── screenshot-simpler-layout.png
├── server-mode.md
├── tools
└── outbox-cleanup
│ ├── commands
│ └── cleanup.js
│ ├── index.js
│ ├── package-lock.json
│ ├── package.json
│ └── readme.md
└── webxdc
├── .gitignore
├── Makefile
├── icon.png
├── icon.svg
├── manifest.toml
└── readme.md
/.gitignore:
--------------------------------------------------------------------------------
1 | .data
2 | .docs
3 | .astro
4 | node_modules
5 | example-data
6 |
7 | # config file
8 | dev/public/js/config.js
9 | dist/js/config.js
10 |
11 | # unused Astro generated files
12 | dist/content-assets.mjs
13 | dist/content-modules.mjs
14 | dist/data-store.json
15 | dist/settings.json
16 |
--------------------------------------------------------------------------------
/.nojekyll:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | _colors*.scss
2 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM nginx:latest
2 |
3 | COPY index.html /usr/share/nginx/html/
4 | COPY dist/ /usr/share/nginx/html/dist/
5 |
6 | EXPOSE 80
7 |
--------------------------------------------------------------------------------
/dev/README.md:
--------------------------------------------------------------------------------
1 | # Astro Starter Kit: Basics
2 |
3 | ```sh
4 | npm create astro@latest -- --template basics
5 | ```
6 |
7 | [](https://stackblitz.com/github/withastro/astro/tree/latest/examples/basics)
8 | [](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/basics)
9 | [](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/basics/devcontainer.json)
10 |
11 | > 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
12 |
13 | 
14 |
15 | ## 🚀 Project Structure
16 |
17 | Inside of your Astro project, you'll see the following folders and files:
18 |
19 | ```text
20 | /
21 | ├── public/
22 | │ └── favicon.svg
23 | ├── src/
24 | │ ├── layouts/
25 | │ │ └── Layout.astro
26 | │ └── pages/
27 | │ └── index.astro
28 | └── package.json
29 | ```
30 |
31 | To learn more about the folder structure of an Astro project, refer to [our guide on project structure](https://docs.astro.build/en/basics/project-structure/).
32 |
33 | ## 🧞 Commands
34 |
35 | All commands are run from the root of the project, from a terminal:
36 |
37 | | Command | Action |
38 | | :------------------------ | :----------------------------------------------- |
39 | | `npm install` | Installs dependencies |
40 | | `npm run dev` | Starts local dev server at `localhost:4321` |
41 | | `npm run build` | Build your production site to `./dist/` |
42 | | `npm run preview` | Preview your build locally, before deploying |
43 | | `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
44 | | `npm run astro -- --help` | Get help using the Astro CLI |
45 |
46 | ## 👀 Want to learn more?
47 |
48 | Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).
49 |
--------------------------------------------------------------------------------
/dev/astro.config.mjs:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | import { defineConfig } from "astro/config";
3 |
4 | import relativeLinks from "astro-relative-links";
5 |
6 | import compress from "astro-compress";
7 |
8 | // https://astro.build/config
9 | export default defineConfig({
10 | integrations: [
11 | relativeLinks(),
12 | compress({
13 | Exclude: ["alpinejs-i18n.min.js", "alpinejs.min.js", "jszip.min.js", "config.js", "config.default.js"],
14 | }),
15 | ],
16 | outDir: "../dist",
17 | });
18 |
--------------------------------------------------------------------------------
/dev/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "marl",
3 | "type": "module",
4 | "version": "0.0.1",
5 | "scripts": {
6 | "dev": "astro dev",
7 | "build": "astro build",
8 | "preview": "astro preview",
9 | "astro": "astro"
10 | },
11 | "dependencies": {
12 | "astro": "^5.7.0",
13 | "astro-compress": "^2.3.6",
14 | "astro-relative-links": "^0.4.2"
15 | },
16 | "devDependencies": {
17 | "sass-embedded": "^1.83.0"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/dev/public/js/MARL/init.js:
--------------------------------------------------------------------------------
1 | // Note: Alpine plug-ins must be inserted BEFORE alpinejs
2 | loadScript("alpine-i18n");
3 | loadScript("alpine");
4 |
5 | document.addEventListener("alpine:init", () => {
6 | // create and init stores
7 | Alpine.store("files", filesStore);
8 | Alpine.store("lightbox", lightboxStore);
9 | Alpine.store("ui", uiStore);
10 | Alpine.store("userPrefs", userPrefsStore);
11 |
12 | const salutations = [
13 | "Hi!",
14 | "Hiya!",
15 | "Hello there!",
16 | "Good day!",
17 | "Hullo!",
18 | "Buongiorno!",
19 | "Guten Tag!",
20 | "Bonjour!",
21 | "Oh hey!",
22 | ];
23 | marlConsole(`MARL loaded. ${salutations[Math.floor(Math.random() * salutations.length)]} 😊`);
24 |
25 | checkMobileLayout();
26 | setMarlMode();
27 | loadCustomPrefs();
28 | resetStores();
29 |
30 | if (serverMode()) {
31 | initServerMode();
32 | } else {
33 | initLocalMode();
34 | }
35 | });
36 |
37 | // i18n
38 |
39 | document.addEventListener("alpine-i18n:ready", function () {
40 | AlpineI18n.create("en", appStrings);
41 | AlpineI18n.fallbackLocale = "en";
42 | setLang();
43 | });
44 |
--------------------------------------------------------------------------------
/dev/public/js/MARL/libs.js:
--------------------------------------------------------------------------------
1 | const isFileProtocol = window.location.protocol === "file:";
2 |
3 | const scripts = {
4 | jszip: {
5 | src: "js/libs/jszip.min.js",
6 | integrity: "sha512-XMVd28F1oH/O71fzwBnV7HucLxVwtxf26XV8P4wPk26EDxuGZ91N8bsOttmnomcCD3CS5ZMRL50H0GgOHvegtg==",
7 | crossorigin: "anonymous",
8 | defer: false,
9 | },
10 | "alpine-i18n": {
11 | src: "js/libs/alpinejs-i18n.min.js",
12 | integrity: "sha256-o204NcFyHPFzboSC51fufMqFe2KJdQfSCl8AlvSZO/E=",
13 | crossorigin: "anonymous",
14 | defer: true,
15 | },
16 | alpine: {
17 | src: "js/libs/alpinejs.min.js",
18 | integrity: "sha512-FUaEyIgi9bspXaH6hUadCwBLxKwdH7CW24riiOqA5p8hTNR/RCLv9UpAILKwqs2AN5WtKB52CqbiePBei3qjKg==",
19 | crossorigin: "anonymous",
20 | defer: true,
21 | },
22 | };
23 |
24 | function loadScript(scriptName) {
25 | const s = scripts[scriptName];
26 |
27 | const script = document.createElement("script");
28 | script.src = s.src;
29 | if (!isFileProtocol) {
30 | script.integrity = s.integrity;
31 | script.crossOrigin = s.crossorigin;
32 | }
33 | if (s.defer) {
34 | script.defer = true;
35 | }
36 | document.head.appendChild(script);
37 | }
38 |
--------------------------------------------------------------------------------
/dev/public/js/config.default.js:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | This file is used to determine whether MARL operates in LOCAL or SERVER mode.
4 | For more information, please read the "server-mode.md" documentation file.
5 |
6 | Important: please rename this file "config.js" if you want to use it.
7 |
8 | 1. local mode
9 | =============
10 |
11 | If the "servers" constant (below) is absent or empty, (or if this file is removed or renamed) MARL will work in LOCAL mode.
12 | Default value:
13 |
14 | const servers = [];
15 |
16 | 2. server mode
17 | ==============
18 |
19 | Use the "servers" constant below to indicate the path(s) where MARL can find your archive(s).
20 |
21 | If a path is specified, MARL will automatically switch to server mode.
22 |
23 | Relative paths are authorized. They should be relative to MARL (index.html), not to this config file.
24 |
25 | Example:
26 |
27 | const servers = [
28 | "../data/archive1/",
29 | "../data/archive2/",
30 | "https://my-other-server.net/some-other-path/",
31 | ];
32 |
33 | Each archive file must be already unpacked on the server. The "actor.json" and "outbox.json" files should be stored directly under the specified path.
34 |
35 | You may specify as many archives as you want. You can store them wherever you want. In case the archive is stored on a different server from MARL itself, make sure that the server has CORS enabled.
36 |
37 | */
38 |
39 | const servers = [];
40 |
41 | /*
42 |
43 | You can use the following object to change the default options for the app (those are the options found in the tools panel).
44 | Note: Users will still be able to change those options for themselves.
45 |
46 | const customPrefs = {};
47 |
48 | You can use the following properties. All properties are optional:
49 |
50 | - lang: the language for the UI
51 | possible values: "en" (default), "fr", "es"
52 | note: if no option is set, the app will attempt to use the same language as the user's browser (if available for the app)
53 |
54 | - theme: the theme for the app
55 | possible values: "light" (default), "dark"
56 | note: if no option is set, the app will follow the user preference (set in their browser or OS)
57 |
58 | - sortAsc: the order of the posts (chronological or not); true means "oldest posts first"
59 | possible values: true (default), false
60 |
61 | - pageSize: the number of posts per page
62 | possibles values: any number above 0; default is 10; be mindful that large values may slow down the app significantly
63 |
64 | - combinePanels: (on large screens) combine the panels into one sidebar
65 | possible values: true, false (default)
66 |
67 | - defaultPanel: if "combinePanels" is active (true), which panel is displayed upon loading the app
68 | possible values: "actor", "filters", "tags", "tools"
69 | note: if "combinePanels" is active and if this option is not set, no panel will be initially displayed
70 |
71 | - simplifyPostsDisplay: hide some technical or redundant elements in posts display
72 | possible values: true, false (default)
73 |
74 | The folloging options are specific to server mode and not present in the tools panel:
75 |
76 | - showActorJson: whether or not to display the full JSON data in the actor panel
77 | possible values: true, false (default)
78 |
79 | - showPostsJson: whether or not to display the full JSON data related to each post
80 | possible values: true, false (default)
81 | note: JSON data for posts is always hidden in "simplify post display" mode
82 |
83 | Example:
84 |
85 | const customPrefs = {
86 | pageSize: 5,
87 | combinePanels: true,
88 | defaultPanel: "actor",
89 | simplifyPostsDisplay: true,
90 | };
91 |
92 | */
93 |
--------------------------------------------------------------------------------
/dev/public/js/i18n/_langs.js:
--------------------------------------------------------------------------------
1 | const appLangs = {
2 | en: "English",
3 | fr: "Français",
4 | es: "Español",
5 | };
6 |
7 | let appStrings = {};
8 |
--------------------------------------------------------------------------------
/dev/public/js/i18n/en.js:
--------------------------------------------------------------------------------
1 | appStrings["en"] = {
2 | welcome: {
3 | title: "Welcome to {appName}",
4 | p1: `MARL allows you to explore the content of your Mastodon archive file in a
5 | user-friendly interface. Everything takes place in the browser: your archive stays
6 | strictly on your computer; none of its data is sent to any server.`,
7 | p2: `You can request your Mastodon archive by logging into your account on the web,
8 | then visiting "Preferences > Import and export > Request your archive".
9 | Please note: only ZIP files are supported (not GZ).`,
10 | p3: `Start by opening your archive file with MARL.
11 | You can drag and drop it anywhere on this page, or
12 | {labelStart}click here to select it{labelEnd}.`,
13 | },
14 | misc: {
15 | loading: "Loading",
16 | criticalFailure: "Critical failure",
17 | closePanelBtn: "Close panel",
18 | },
19 | menu: {
20 | profile: "Profile",
21 | filters: "Filters",
22 | tags: "Tags",
23 | tools: "Tools",
24 | filtersActive: "some filters are active",
25 | },
26 | lightbox: {
27 | next: "Next image",
28 | prev: "Previous image",
29 | close: "Close image",
30 | },
31 | actor: {
32 | accountInfo: "Account info",
33 | accounts: "Accounts",
34 | noAvatarImage: "No avatar image",
35 | noHeaderImage: "No header image",
36 | headerImage: "Header",
37 | memberSince: "Member since",
38 | countPosts: "posts",
39 | countInArchive: "in archive",
40 | countDiffWhy: "Why are those two numbers different?",
41 | countDiffExplanation: `Posts that are not directly hosted on your instance are kept
42 | in a cache by your instance for a given time, after what they are deleted from that
43 | cache. Posts that are not in your instance cache any more are not included in your
44 | archive. This affects boosts, likes, and bookmarks.`,
45 | rawData: "Raw data {fileName}",
46 | likes: "Favorites",
47 | likesEmpty: "no favorites",
48 | bookmarks: "Bookmarks",
49 | bookmarksEmpty: "no bookmarks",
50 | },
51 | filters: {
52 | panelTitle: "Filter posts",
53 | panelNotice: `The list of posts will be automatically updated based on the active
54 | filters below.`,
55 | fullText: "Full text",
56 | hashtagText: "Hashtags",
57 | mentionText: "Mentions",
58 | externalLink: "External links",
59 | summary: "Summary (CW)",
60 | isDuplicate: "Non-exact duplicates",
61 |
62 | mustContain: "Must contain",
63 | hasHashtags: "Hashtag(s)",
64 | hasMentions: "Mention(s)",
65 | hasPoll: "Poll",
66 | hasExternalLink: "External link(s)",
67 | hasSummary: "Summary (CW)",
68 |
69 | activities: "Activities",
70 | hasLikes: "likes or more",
71 | hasShares: "shares or more",
72 |
73 | dateTime: "Date & time",
74 | afterDate: "or later",
75 | beforeDate: "or earlier",
76 | afterTime: "or later",
77 | beforeTime: "or earlier",
78 |
79 | type: "Type",
80 | typeOriginal: "Original posts (incl. replies)",
81 | typeBoost: "Boosts",
82 | startingAt: 'Starts with "@"',
83 | noStartingAt: 'Does not start with "@"',
84 | isInConversation: "Is part of a thread",
85 | isSensitive: "Marked as sensitive",
86 | isEdited: "Has been edited",
87 |
88 | mustHaveAttachement: "Must have attachment",
89 | attachmentAny: "Any type",
90 | attachmentImage: "Image(s)",
91 | attachmentVideo: "Video(s)",
92 | attachmentSound: "Sound(s)",
93 | attachmentNoAltText: "Without alt text",
94 | attachmentWithAltText: "With alt text",
95 |
96 | visibility: "Visibility",
97 | visibilityPublic: "Public",
98 | visibilityUnlisted: "Quiet public (Unlisted)",
99 | visibilityFollowers: "Followers only",
100 | visibilityMentioned: "Mentioned people only",
101 |
102 | language: "Language",
103 | author: "Author",
104 |
105 | showResults: "Show results",
106 | resetFilters: "Reset filters",
107 | },
108 | header: {
109 | countLabel: "posts",
110 | oldestFirst: "oldest first",
111 | latestFirst: "latest first",
112 | },
113 | paging: {
114 | first: "First",
115 | prev: "Prev",
116 | next: "Next",
117 | last: "Last",
118 | page: "Page",
119 | },
120 | posts: {
121 | panelTitle: "Posts",
122 | noResults: "No results for the specified filters",
123 | noPostsError: "No posts found in archive",
124 | },
125 | post: {
126 | showConversation: "Show thread",
127 | by: "by",
128 | lastUpdated: "Last updated",
129 | linkToPost: "Link",
130 | pollType: "Poll type:",
131 | pollTypeOne: "One choice only",
132 | pollTypeAny: "Multiple choices",
133 | pollVotersCount: "Total voters:",
134 | pollVotersCountVotes: "({nb} votes)",
135 | pollVotersCountAlt: "{nb} voters",
136 | pollEndTime: "End time:",
137 | pollStatus: "Status:",
138 | pollStatusClosed: "Closed",
139 | pollStatusRunning: "Running",
140 | attachmentNoAlt: "No description provided",
141 | attachmentInArchive: "In archive:",
142 | attachmentNotFound: "⚠️ Content not found in archive",
143 | like1: "1 like",
144 | like2: "{nb} likes",
145 | share1: "1 share",
146 | share2: "{nb} shares",
147 | people: "People",
148 | hashtags: "Hashtags",
149 | extLinks: "External links",
150 | rawData: "Raw data",
151 | },
152 | conversation: {
153 | panelTitle: "Thread",
154 | panelTitleNbPosts: "({nb} posts in archive)",
155 | closePanel: "Close thread",
156 | helpSummary: "Why is this thread broken or incomplete?",
157 | helpContent:
158 | "MARL can only show you posts that are present in your archive. Posts written by other people are typically not included in an archive. If posts are missing in a thread, it's impossible for MARL to reconnect the broken parts. In other words, what you will mostly see here are posts where you reply to yourself (aka threads 🧵).",
159 | },
160 | tags: {
161 | panelTitle: "Tags",
162 | hashtags: "Hashtags",
163 | mentions: "Mentions",
164 | boosts: "Boosted users",
165 | hashtagsFilter: "Filter hashtags",
166 | mentionsFilter: "Filter mentions",
167 | boostsFilter: "Filter boosted users",
168 | resetFilter: "reset filter",
169 | },
170 | tools: {
171 | panelTitle: "Tools",
172 | appSettings: "App settings",
173 | selectLanguage: "Select language",
174 | useDarkTheme: "Use dark theme",
175 | useLightTheme: "Use light theme",
176 | panelsSettings: "Panels",
177 | combinePanels: "Combine panels",
178 | combinePanelsDesc: "(Wide screens only) Combine all panels into a single sidebar with vertical tabs",
179 | defaultPanel: "Default panel",
180 | defaultPanelDesc: "Which panel is shown by default when opening the app (if the panels are combined)",
181 | lastPanel: "Last panel used",
182 | postsSettings: "Posts",
183 | postsPerPage: "Number of posts per page:",
184 | chronologicalOrder: "Chronological order",
185 | chronologicalOrderDesc: "Uncheck to sort posts in reverse chronological order (most recent first)",
186 | simplifyPostsDisplay: "Simplify posts display",
187 | simplifyPostsDisplayDesc: "Hide some technical or redundant elements",
188 | loadedFiles: "Loaded files",
189 | loadedRemotes: "Loaded archives",
190 | addAnother: "Add another archive",
191 | addAnotherTip:
192 | "Tip: You can open multiple archives at once.
You can also drag and drop your archive files anywhere on this window.",
193 | startOver: "Start over",
194 | startOverConfirm: "Discard current data and load a new archive file?",
195 | appLog: "App log",
196 | projectPage: `Project page (github)`,
197 | },
198 | };
199 |
--------------------------------------------------------------------------------
/dev/public/js/i18n/es.js:
--------------------------------------------------------------------------------
1 | appStrings["es"] = {
2 | welcome: {
3 | title: "Bienvenido a {appName}",
4 | p1: `MARL le permite explorar el contenido de su archivo Mastodon en un
5 | interfaz fácil de usar. Todo tiene lugar en el navegador: su archivo permanece
6 | estrictamente en su ordenador; ninguno de sus datos se envía a ningún servidor.`,
7 | p2: `Puede solicitar su archivo de Mastodon accediendo a su cuenta en la web,
8 | y visitando «Preferencias > Importar y exportar > Solicitar su archivo»..
9 | Nota: sólo se admiten archivos ZIP (no GZ).`,
10 | p3: `Empieza abriendo tu archivo con MARL.
11 | Puede arrastrarlo y soltarlo en cualquier lugar de esta página, o
12 | {labelStart}haga clic aquí para seleccionarlo{labelEnd}.`,
13 | },
14 | misc: {
15 | loading: "Cargando",
16 | criticalFailure: "fallo crítico",
17 | closePanelBtn: "Cerrar panel",
18 | },
19 | menu: {
20 | profile: "Perfil",
21 | filters: "Filtros",
22 | tags: "Etiquetas",
23 | tools: "Herramientas",
24 | filtersActive: "algunos filtros están activos",
25 | },
26 | lightbox: {
27 | next: "Imagen siguiente",
28 | prev: "imagen previa",
29 | close: "Cerrar imagen",
30 | },
31 | actor: {
32 | accountInfo: "Info de cuenta",
33 | accounts: "Cuentas",
34 | noAvatarImage: "Sin avatar",
35 | noHeaderImage: "Sin imagen de cabecera",
36 | headerImage: "Cabecera",
37 | memberSince: "Miembro desde",
38 | countPosts: "publicaciones",
39 | countInArchive: "en el archivo",
40 | countDiffWhy: "¿Por qué son estos dos números diferentes?",
41 | countDiffExplanation: `Los mensajes que no se alojan directamente en su instancia se mantienen
42 | en una caché durante un tiempo determinado, después de lo cual se eliminan.
43 | Los mensajes que ya no están en la caché de tu instancia no se incluyen en tu archivo.
44 | Esto afecta a los impulsos, favoritos y marcadores.`,
45 | rawData: "Datos en bruto {fileName}",
46 | likes: "Favoritos",
47 | likesEmpty: "sin favoritos",
48 | bookmarks: "Marcadores",
49 | bookmarksEmpty: "sin marcadores",
50 | },
51 | filters: {
52 | panelTitle: "Filtrar publicaciones",
53 | panelNotice: `La lista de entradas se actualizará automáticamente en función
54 | de los siguientes filtros activos.`,
55 | fullText: "Texto completo",
56 | hashtagText: "Etiquetas",
57 | mentionText: "Menciones",
58 | externalLink: "Enlaces externos",
59 | summary: "Aviso de contenido",
60 | isDuplicate: "Duplicado no-exacto",
61 |
62 | mustContain: "debe contener",
63 | hasHashtags: "Etiquetas(s)",
64 | hasMentions: "Mencion(es)",
65 | hasPoll: "Encuesta",
66 | hasExternalLink: "Enlace(s) externo(s)",
67 | hasSummary: "Aviso de contenido",
68 |
69 | activities: "Actividades",
70 | hasLikes: "favoritos o más",
71 | hasShares: "impulsos o más",
72 |
73 | dateTime: "Fecha & hora",
74 | afterDate: "o después",
75 | beforeDate: "o antes",
76 | afterTime: "o después",
77 | beforeTime: "o antes",
78 |
79 | type: "Tipo",
80 | typeOriginal: "Publicación original (incl. respuestas)",
81 | typeBoost: "Impulsos",
82 | startingAt: 'Empieza por "@"',
83 | noStartingAt: 'No empieza por "@"',
84 | isInConversation: "Es parte de un hilo",
85 | isSensitive: "Marcado como sensible",
86 | isEdited: "Editado",
87 |
88 | mustHaveAttachement: "Debe tener adjunto",
89 | attachmentAny: "Cualquier tipo",
90 | attachmentImage: "Imagen(es)",
91 | attachmentVideo: "Vídeo(s)",
92 | attachmentSound: "Sonido(s)",
93 | attachmentNoAltText: "Sin texto alt",
94 | attachmentWithAltText: "Con texto alt",
95 |
96 | visibility: "Visibilidad",
97 | visibilityPublic: "Público",
98 | visibilityUnlisted: "Silencioso (no listado)",
99 | visibilityFollowers: "Sólo seguidores",
100 | visibilityMentioned: "Sólo cuentas mencionadas",
101 |
102 | language: "Idioma",
103 | author: "Autor",
104 |
105 | showResults: "Mostrar resultados",
106 | resetFilters: "Reiniciar filtros",
107 | },
108 | header: {
109 | countLabel: "publicaciones",
110 | oldestFirst: "desde más antiguo",
111 | latestFirst: "desde más reciente",
112 | },
113 | paging: {
114 | first: "Inicio",
115 | prev: "Anterior",
116 | next: "Siguiente",
117 | last: "Fin",
118 | page: "Página",
119 | },
120 | posts: {
121 | panelTitle: "Publicaciones",
122 | noResults: "Sin resultados para los filtros indicados",
123 | noPostsError: "No se encontraron publicaciones en el archivo",
124 | },
125 | post: {
126 | showConversation: "Mostrar hilo",
127 | by: "por",
128 | lastUpdated: "Última modificación",
129 | linkToPost: "Enlace",
130 | pollType: "Tipo de encuesta:",
131 | pollTypeOne: "Opción única",
132 | pollTypeAny: "Opciones múltiples",
133 | pollVotersCount: "Votantes totales:",
134 | pollVotersCountVotes: "({nb} votos)",
135 | pollVotersCountAlt: "{nb} votantes",
136 | pollEndTime: "Termina:",
137 | pollStatus: "Status:",
138 | pollStatusClosed: "Cerrada",
139 | pollStatusRunning: "En curso",
140 | attachmentNoAlt: "Sin descripción",
141 | attachmentInArchive: "En el archivo:",
142 | attachmentNotFound: "⚠️ Contenido no encontrado en el archivo",
143 | like1: "1 favorito",
144 | like2: "{nb} favoritos",
145 | share1: "1 impulso",
146 | share2: "{nb} impulsos",
147 | people: "Gente",
148 | hashtags: "Etiquetas",
149 | extLinks: "Enlaces externos",
150 | rawData: "Datos en bruto",
151 | },
152 | conversation: {
153 | panelTitle: "Hilo",
154 | panelTitleNbPosts: "({nb} publicaciones en archivo)",
155 | closePanel: "Cerrar hilo",
156 | helpSummary: "¿Porque está roto o incompleto este hilo?",
157 | helpContent:
158 | "MARL sólo puede mostrarte los mensajes que están presentes en tu archivo. Los mensajes escritos por otras personas normalmente no se incluyen en un archivo. Si faltan mensajes en un hilo, es imposible para MARL reconectar las partes rotas. En otras palabras, lo que verás aquí son mensajes en los que te respondes a ti mismo (es decir, hilos 🧵).",
159 | },
160 | tags: {
161 | panelTitle: "Etiquetas",
162 | hashtags: "Etiquetas",
163 | mentions: "Menciones",
164 | boosts: "Usuarios impulsados",
165 | hashtagsFilter: "Filtrar etiquetas",
166 | mentionsFilter: "Filtrar menciones",
167 | boostsFilter: "Filtrar usuarios impulsados",
168 | resetFilter: "reiniciar filtro",
169 | },
170 | tools: {
171 | panelTitle: "Herramientas",
172 | appSettings: "Ajustes de aplicación",
173 | selectLanguage: "Seleccionar idioma",
174 | useDarkTheme: "Usar tema oscuro",
175 | useLightTheme: "Usar tema claro",
176 | panelsSettings: "Paneles",
177 | combinePanels: "Combinar paneles",
178 | combinePanelsDesc:
179 | "(Sólo pantalla anchas) Combinar todos los paneles en una una barra lateral única con pestañas verticales",
180 | defaultPanel: "Panel por defecto",
181 | defaultPanelDesc: "Qué panel se muestra por defecto al abrir la aplicación (si hay paneles combinados)",
182 | lastPanel: "Último panel usado",
183 | postsSettings: "Publicaciones",
184 | postsPerPage: "Número de publicaciones por página:",
185 | chronologicalOrder: "Orden cronológico",
186 | chronologicalOrderDesc:
187 | "Desmarca para ordenar las publicaciones en orden cronológico inverso (más recientes primero)",
188 | simplifyPostsDisplay: "Simplificar visualización de publicaciones",
189 | simplifyPostsDisplayDesc: "Ocultar algunos elementos técnicos o redundantes",
190 | loadedFiles: "Archivos cargados",
191 | loadedRemotes: "Archivos cargados",
192 | addAnother: "Añadir otro archivo",
193 | addAnotherTip:
194 | "Consejo: Puede abrir varios archivos al mismo tiempo.
También puede arrastrar y soltar sus ficheros comprimidos en cualquier lugar de esta ventana.",
195 | startOver: "Reiniciar",
196 | startOverConfirm: "¿Descartar los datos actuales y cargar un nuevo archivo?",
197 | appLog: "Registro de la aplicación",
198 | projectPage: `Página del proyecto (github)`,
199 | },
200 | };
201 |
--------------------------------------------------------------------------------
/dev/public/js/i18n/fr.js:
--------------------------------------------------------------------------------
1 | appStrings["fr"] = {
2 | welcome: {
3 | title: "Bienvenue dans {appName}",
4 | p1: `MARL vous permet d'explorer le contenu de votre archive Mastodon dans une
5 | interface simple d'utilisation. Tout se passe dans votre navigateur : votre archive
6 | reste sur votre appareil; aucune donnée n'est envoyée à aucun serveur.`,
7 | p2: `Vous pouvez demander votre archive Mastodon en vous identifiant avec votre compte
8 | sur le web, puis en visitant "Préférences >Import et export >Demandez vos archives".
9 | Important : seuls les fichiers ZIP sont acceptés (pas les fichiers GZ).`,
10 | p3: `Commencez par ouvrir votre archive avec MARL.
11 | Vous pouvez la glisser-déposer n'importe où sur cette page, ou
12 | {labelStart}cliquer ici pour la sélectionner{labelEnd}.`,
13 | },
14 | misc: {
15 | loading: "Chargement",
16 | criticalFailure: "Erreur fatale",
17 | closePanelBtn: "Fermer le panneau",
18 | },
19 | menu: {
20 | profile: "Profil",
21 | filters: "Filtres",
22 | tags: "Tags",
23 | tools: "Outils",
24 | filtersActive: "certains filtres sont actifs",
25 | },
26 | lightbox: {
27 | next: "Image suivante",
28 | prev: "Image précédente",
29 | close: "Fermer l'image",
30 | },
31 | actor: {
32 | accountInfo: "Infos du compte",
33 | accounts: "Comptes",
34 | noAvatarImage: "Pas d'avatar",
35 | noHeaderImage: "pas d'image d'en-tête",
36 | headerImage: "En-tête",
37 | memberSince: "Membre depuis",
38 | countPosts: "posts",
39 | countInArchive: "dans l'archive",
40 | countDiffWhy: "Pourquoi ces deux nombres sont-ils différents ?",
41 | countDiffExplanation: `Les posts qui ne sont pas hébergés directement sur votre instance
42 | sont gardés en cache par celle-ci pour une durée limitée, après quoi ils sont supprimés
43 | de ce cache. Les posts qui ne sont plus présents dans le cache de votre instance ne sont
44 | pas inclus dans votre archive. Cela concerne les partages, les favoris et les marque-pages.`,
45 | rawData: "Données brutes {fileName}",
46 | likes: "Favoris",
47 | likesEmpty: "aucun favori",
48 | bookmarks: "Marque-pages",
49 | bookmarksEmpty: "aucun marque-page",
50 | },
51 | filters: {
52 | panelTitle: "Filtrer les posts",
53 | panelNotice: `La liste des posts sera automatiquement mise à jour en fonction des filtres
54 | activés ci-dessous.`,
55 | fullText: "Partout",
56 | hashtagText: "Hashtags",
57 | mentionText: "Mentions",
58 | externalLink: "Liens externes",
59 | summary: "Avertissement de contenu",
60 | isDuplicate: "Doublons imparfaits",
61 |
62 | mustContain: "Doit contenir",
63 | hasHashtags: "Hashtag(s)",
64 | hasMentions: "Mention(s)",
65 | hasPoll: "Sondage",
66 | hasExternalLink: "Lien(s) externe(s)",
67 | hasSummary: "Avertissement de contenu",
68 |
69 | activities: "Activités",
70 | hasLikes: "favoris ou plus",
71 | hasShares: "partages ou plus",
72 |
73 | dateTime: "Date & heure",
74 | afterDate: "ou plus tard",
75 | beforeDate: "ou plus tôt",
76 | afterTime: "ou plus tard",
77 | beforeTime: "ou plus tôt",
78 |
79 | type: "Type",
80 | typeOriginal: "Posts originaux (y.c. réponses)",
81 | typeBoost: "Partages",
82 | startingAt: 'Commence par "@"',
83 | noStartingAt: 'Ne commence pas par "@"',
84 | isInConversation: "Fait partie d'un fil",
85 | isSensitive: "Marqué comme sensible",
86 | isEdited: "A été modifié",
87 |
88 | mustHaveAttachement: "Doit avoir un fichier joint",
89 | attachmentAny: "N'importe quel type",
90 | attachmentImage: "Image(s)",
91 | attachmentVideo: "Vidéo(s)",
92 | attachmentSound: "Son(s)",
93 | attachmentNoAltText: "Sans description alternative",
94 | attachmentWithAltText: "Avec description alternative",
95 |
96 | visibility: "Confidentialité",
97 | visibilityPublic: "Public",
98 | visibilityUnlisted: "Public discret",
99 | visibilityFollowers: "Abonnés",
100 | visibilityMentioned: "Personnes spécifiques",
101 |
102 | language: "Langue",
103 | author: "Auteur",
104 |
105 | showResults: "Afficher les résultats",
106 | resetFilters: "Réinitialiser les filtres",
107 | },
108 | header: {
109 | countLabel: "posts",
110 | oldestFirst: "les plus anciens d'abord",
111 | latestFirst: "les plus récents d'abord",
112 | },
113 | paging: {
114 | first: "Première",
115 | prev: "Précédente",
116 | next: "Suivante",
117 | last: "Dernière",
118 | page: "Page",
119 | },
120 | posts: {
121 | panelTitle: "Posts",
122 | noResults: "Pas de résultats pour les filtres spécifiés",
123 | noPostsError: "Aucun post trouvé dans l'archive",
124 | },
125 | post: {
126 | showConversation: "Voir le fil",
127 | by: "par",
128 | lastUpdated: "Dernière modification",
129 | linkToPost: "Lien",
130 | pollType: "Type de sondage :",
131 | pollTypeOne: "Choix unique",
132 | pollTypeAny: "Choix multiples",
133 | pollVotersCount: "Nombre de votants :",
134 | pollVotersCountVotes: "({nb} votes)",
135 | pollVotersCountAlt: "{nb} votants",
136 | pollEndTime: "Date de fin :",
137 | pollStatus: "État :",
138 | pollStatusClosed: "Terminé",
139 | pollStatusRunning: "En cours",
140 | attachmentNoAlt: "Aucune description fournie",
141 | attachmentInArchive: "Dans l'archive :",
142 | attachmentNotFound: "⚠️ Média introuvable à l'emplacement indiqué",
143 | like1: "1 favori",
144 | like2: "{nb} favoris",
145 | share1: "1 partage",
146 | share2: "{nb} partages",
147 | people: "Personnes",
148 | hashtags: "Hashtags",
149 | extLinks: "Liens externes",
150 | rawData: "Données brutes",
151 | },
152 | conversation: {
153 | panelTitle: "Fil",
154 | panelTitleNbPosts: "({nb} posts dans l'archive)",
155 | closePanel: "Fermer le fil",
156 | helpSummary: "Pourquoi ce fil est-il cassé ou incomplet ?",
157 | helpContent:
158 | "MARL peut seulement afficher les posts qui sont présents dans votre archive. De manière générale, les posts rédigés par d'autres personnes ne font pas partie du contenu d'une archive. Si un fil est cassé du fait de l'absence d'un post, il est impossible pour MARL de reconnecter les fragments. En d'autres termes, ce que vous verrez ici, ce sont principalement des réponses à vous-même (autrement dit, des fils ou threads 🧵).",
159 | },
160 | tags: {
161 | panelTitle: "Tags",
162 | hashtags: "Hashtags",
163 | mentions: "Mentions",
164 | boosts: "Utilisateurs partagés",
165 | hashtagsFilter: "Filtrer les hashtags",
166 | mentionsFilter: "Filtrer les mentions",
167 | boostsFilter: "Filter utilisateurs partagés",
168 | resetFilter: "réinitialiser le filtre",
169 | },
170 | tools: {
171 | panelTitle: "Outils",
172 | appSettings: "Réglages de l'app",
173 | selectLanguage: "Choisir la langue",
174 | useDarkTheme: "Utiliser le thème sombre",
175 | useLightTheme: "Utiliser le thème clair",
176 | panelsSettings: "Panneaux",
177 | combinePanels: "Combiner les panneaux",
178 | combinePanelsDesc:
179 | "(Sur les écrans larges) Combiner les différents panneaux en une seule barre latérale avec onglets verticaux",
180 | defaultPanel: "Panneau par défaut",
181 | defaultPanelDesc: "Quel panneau est affiché par défaut au chargement de l'app (si les panneaux sont combinés)",
182 | lastPanel: "Dernier panneau utilisé",
183 | postsSettings: "Posts",
184 | postsPerPage: "Nombre de posts page page :",
185 | chronologicalOrder: "Ordre chronologique",
186 | chronologicalOrderDesc:
187 | "Décocher pour trier les posts dans l'ordre chronologique inverse (les plus récents en premier)",
188 | simplifyPostsDisplay: "Simplifier l'affichage des posts",
189 | simplifyPostsDisplayDesc: "Cacher certains éléments techniques ou redondants",
190 | loadedFiles: "Fichiers chargés",
191 | loadedRemotes: "Archives chargées",
192 | addAnother: "Ajouter une autre archive",
193 | addAnotherTip:
194 | "Astuce: Vous pouvez ouvrir plusieurs archives en même temps.
Vous pouvez aussi glisser-déposer vos fichiers d'archive n'importe où dans cette fenêtre.",
195 | startOver: "Recommencer",
196 | startOverConfirm: "Repartir de zéro et charger un nouveau fichier ?",
197 | appLog: "Journal",
198 | projectPage: `Page du project (github)`,
199 | },
200 | };
201 |
--------------------------------------------------------------------------------
/dev/public/js/libs/alpinejs-i18n.min.js:
--------------------------------------------------------------------------------
1 | (()=>{var c=new Event("alpine-i18n:locale-change"),o=new Event("alpine-i18n:ready"),r={version:"2.5.2",set locale(e){this.checkLocale(e),this.currentLocale=e,document.dispatchEvent(c)},get locale(){return this.currentLocale},currentLocale:"",messages:{},fallbackLocale:"",options:{},create(e,i,n){this.messages=i,this.checkLocale(e),this.locale=e,this.options=n},checkLocale(e){if(!Object.keys(this.messages).includes(e))throw new Error(`Alpine I18n: The locale ${e} does not exist.`)},t(e,i){let n="";try{n=e.split(".").reduce((t,l)=>t[l],this.messages[this.locale])}catch{}if(!n&&this.fallbackLocale.length)n=e.split(".").reduce((t,l)=>t[l],this.messages[this.fallbackLocale]);else if(!n)return this.options?.debug?`???${e}`:e;for(let t in i)if(n&&n.replaceAll&&Object.prototype.hasOwnProperty.call(i,t)){let l=i[t],a=new RegExp("{s*("+t+")s*}","g");n=n.replaceAll(a,l)}return this.options?.debug?`[${n}]`:n}};function s(e){window.AlpineI18n=e.reactive(r),document.dispatchEvent(o),e.magic("locale",i=>n=>{if(!n)return window.AlpineI18n.locale;window.AlpineI18n.locale=n}),e.magic("t",i=>(n,t)=>window.AlpineI18n.t(n,t))}document.addEventListener("alpine:initializing",()=>{s(window.Alpine)});})();
2 |
--------------------------------------------------------------------------------
/dev/src/appVersion.js:
--------------------------------------------------------------------------------
1 | const appVersion = "2.10.1";
2 |
3 | export default appVersion;
4 |
--------------------------------------------------------------------------------
/dev/src/components/App.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import WelcomeScreen from './screens/WelcomeScreen.astro';
3 | import LoadingScreen from './screens/LoadingScreen.astro';
4 | import MainScreen from './screens/MainScreen.astro';
5 | ---
6 |
7 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/dev/src/components/LightBox.astro:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 |
17 |
18 |
19 |
24 |
25 |
26 |
27 |
28 |
29 |
39 |
40 |
50 |
51 |
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/dev/src/components/MobileMenu.astro:
--------------------------------------------------------------------------------
1 |
40 |
41 |
48 |
--------------------------------------------------------------------------------
/dev/src/components/PagingBottom.astro:
--------------------------------------------------------------------------------
1 |
2 |
3 |
12 |
21 |
22 |
23 |
32 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/dev/src/components/PagingOptions.astro:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
14 | /
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/dev/src/components/PagingTop.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import PagingOptions from "./PagingOptions.astro";
3 | ---
4 |
5 |
6 |
7 |
17 |
27 |
28 |
29 |
30 |
31 |
32 |
42 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/dev/src/components/panels/ActorPanel.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import CloseBtn from './CloseBtn.astro'
3 | import ImgAvatar from './actor/ImgAvatar.astro'
4 | import ImgHeader from './actor/ImgHeader.astro'
5 | import LikesBookmarks from './actor/LikesBookmarks.astro'
6 | ---
7 |
8 |
145 |
--------------------------------------------------------------------------------
/dev/src/components/panels/CloseBtn.astro:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/dev/src/components/panels/ConversationPanel.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import Post from './posts/Post.astro';
3 | ---
4 |
5 |
13 |
14 |
33 |
34 |
39 |
40 |
41 |
42 |
43 |
44 |
47 |
48 |
49 |
50 |
53 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/dev/src/components/panels/FiltersPanel.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import CloseBtn from './CloseBtn.astro'
3 | import FilterText from './filters/FilterText.astro';
4 | import FilterCheckbox from './filters/FilterCheckbox.astro';
5 | import FilterActivity from './filters/FilterActivity.astro';
6 | import FilterDate from './filters/FilterDate.astro';
7 | import FilterTime from './filters/FilterTime.astro';
8 | ---
9 |
10 |
200 |
--------------------------------------------------------------------------------
/dev/src/components/panels/HeaderPanel.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import PagingTop from '../PagingTop.astro';
3 | ---
4 |
5 |
27 |
--------------------------------------------------------------------------------
/dev/src/components/panels/PostsPanel.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import PagingBottom from '../PagingBottom.astro';
3 | import Post from './posts/Post.astro';
4 | ---
5 |
6 |
14 |
19 |
20 |
24 |
25 |
26 |
27 |
31 |
32 |
33 |
34 |
43 |
44 |
45 |
48 |
49 |
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/dev/src/components/panels/TagsPanel.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import CloseBtn from './CloseBtn.astro'
3 | import TagsGroup from './tags/TagsGroup.astro';
4 | ---
5 |
6 |
27 |
--------------------------------------------------------------------------------
/dev/src/components/panels/ToolsPanel.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import CloseBtn from './CloseBtn.astro'
3 | import AppSettings from './tools/AppSettings.astro'
4 | import ManageFiles from './tools/ManageFiles.astro'
5 | import RemoteInfos from './tools/RemoteInfos.astro'
6 | import AppLog from './tools/AppLog.astro'
7 | import About from './tools/About.astro'
8 | ---
9 |
10 |
32 |
--------------------------------------------------------------------------------
/dev/src/components/panels/actor/ImgAvatar.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import NoAvatar from '../../../img/no-avatar.png'
3 | import NoAvatarDark from '../../../img/no-avatar-dark.png'
4 | ---
5 |
6 |
7 |
8 |

9 |

10 |
11 |
12 |
13 |
14 |
24 |
25 |
26 |
27 |
37 |
38 |
--------------------------------------------------------------------------------
/dev/src/components/panels/actor/ImgHeader.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import NoHeader from '../../../img/no-header.png'
3 | import NoHeaderDark from '../../../img/no-header-dark.png'
4 | ---
5 |
6 |
7 |
11 |
12 |
13 |
14 |
24 |
25 |
26 |
27 |
37 |
38 |
--------------------------------------------------------------------------------
/dev/src/components/panels/actor/LikesBookmarks.astro:
--------------------------------------------------------------------------------
1 | ---
2 | const { type } = Astro.props;
3 | ---
4 |
5 |
6 |
7 |
8 |
9 |
10 |
13 |
14 |
15 |
16 | ()
17 |
18 |
19 |
20 |
21 |
22 |
23 | -
24 |
25 |
26 |
27 |
28 |
29 |
30 | … …
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/dev/src/components/panels/filters/FilterActivity.astro:
--------------------------------------------------------------------------------
1 | ---
2 | const { name } = Astro.props;
3 | ---
4 |
5 |
29 |
--------------------------------------------------------------------------------
/dev/src/components/panels/filters/FilterCheckbox.astro:
--------------------------------------------------------------------------------
1 | ---
2 | const { name } = Astro.props;
3 | ---
4 |
5 |
10 |
22 |
23 |
--------------------------------------------------------------------------------
/dev/src/components/panels/filters/FilterDate.astro:
--------------------------------------------------------------------------------
1 | ---
2 | const { name } = Astro.props;
3 | ---
4 |
5 |
27 |
--------------------------------------------------------------------------------
/dev/src/components/panels/filters/FilterText.astro:
--------------------------------------------------------------------------------
1 | ---
2 | const { name } = Astro.props;
3 | ---
4 |
5 |
9 |
14 |
15 |
16 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/dev/src/components/panels/filters/FilterTime.astro:
--------------------------------------------------------------------------------
1 | ---
2 | const { name } = Astro.props;
3 | ---
4 |
5 |
27 |
--------------------------------------------------------------------------------
/dev/src/components/panels/posts/AttachmentDescription.astro:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 | ()
9 |
10 |
11 |
16 |
23 |
24 |
25 |
26 |
29 |
30 |
31 |
32 |
33 |
34 |
42 |
43 |
--------------------------------------------------------------------------------
/dev/src/components/panels/posts/AttachmentImage.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import AttachmentDescription from './AttachmentDescription.astro'
3 | ---
4 |
5 |
8 |
9 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
40 |
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/dev/src/components/panels/posts/AttachmentSound.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import AttachmentDescription from './AttachmentDescription.astro'
3 | ---
4 |
5 |
8 |
21 |
22 |
23 |
26 |
38 |
39 |
--------------------------------------------------------------------------------
/dev/src/components/panels/posts/AttachmentVideo.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import AttachmentDescription from './AttachmentDescription.astro'
3 | ---
4 |
5 |
8 |
21 |
22 |
23 |
26 |
38 |
39 |
--------------------------------------------------------------------------------
/dev/src/components/panels/posts/HasReply.astro:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
--------------------------------------------------------------------------------
/dev/src/components/panels/posts/Poll.astro:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | -
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
40 |
41 |
42 |
43 |
44 | -
45 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/dev/src/components/panels/posts/Post.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import PostCreate from "./PostCreate.astro";
3 | import PostAnnounce from "./PostAnnounce.astro";
4 | import PostInfos from "./PostInfos.astro";
5 | import PostTags from "./PostTags.astro";
6 | import PostRaw from "./PostRaw.astro";
7 | ---
8 |
9 |
14 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/dev/src/components/panels/posts/PostAnnounce.astro:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
10 |
17 |
18 |
--------------------------------------------------------------------------------
/dev/src/components/panels/posts/PostCreate.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import AttachmentImage from "./AttachmentImage.astro";
3 | import AttachmentSound from "./AttachmentSound.astro";
4 | import AttachmentVideo from "./AttachmentVideo.astro";
5 | import HasReply from "./HasReply.astro";
6 | import Poll from "./Poll.astro";
7 | ---
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
21 |
22 |
25 |
26 |
27 |
28 |
29 | -
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/dev/src/components/panels/posts/PostInfos.astro:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
34 |
39 |
40 |
41 |
42 |
43 |
44 |
49 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/dev/src/components/panels/posts/PostRaw.astro:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
9 |
10 |
15 |
16 |
17 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/dev/src/components/panels/posts/PostTags.astro:
--------------------------------------------------------------------------------
1 |
4 |
44 |
45 |
--------------------------------------------------------------------------------
/dev/src/components/panels/tags/TagsGroup.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import TagsGroupHeader from './TagsGroupHeader.astro';
3 | import TagsGroupMain from './TagsGroupMain.astro';
4 | const { type } = Astro.props;
5 | ---
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/dev/src/components/panels/tags/TagsGroupHeader.astro:
--------------------------------------------------------------------------------
1 | ---
2 | const { type } = Astro.props;
3 | let storeEntry, filterId;
4 |
5 | switch (type) {
6 | case 'hashtags':
7 | filterId = 'hashtags';
8 | storeEntry = "listHashtags";
9 | break;
10 | case 'mentions':
11 | filterId = 'mentions';
12 | storeEntry = "listMentions";
13 | break;
14 | case 'boosts':
15 | filterId = 'boostsAuthors';
16 | storeEntry = "listBoostsAuthors";
17 | break;
18 | }
19 |
20 | ---
21 |
56 |
--------------------------------------------------------------------------------
/dev/src/components/panels/tags/TagsGroupMain.astro:
--------------------------------------------------------------------------------
1 | ---
2 | const { type } = Astro.props;
3 | let storeData, storeFilter, filterProp, btnId, clickCmd;
4 |
5 | switch (type) {
6 | case 'hashtags':
7 | storeData = "listHashtags";
8 | storeFilter = "hashtagText";
9 | filterProp = "name";
10 | btnId = "hashtag";
11 | clickCmd = `.slice(1)`;
12 | break;
13 | case 'mentions':
14 | storeData = "listMentions";
15 | storeFilter = "mentionText";
16 | filterProp = "name";
17 | btnId = "mention";
18 | clickCmd = `.replaceAll('@', '_').replaceAll('.', '_')`;
19 | break;
20 | case 'boosts':
21 | storeData = "listBoostsAuthors";
22 | storeFilter = "fullText";
23 | filterProp = "url";
24 | btnId = "boost-author";
25 | clickCmd = `.replaceAll('/', '_')`;
26 | break;
27 | }
28 |
29 | ---
30 |
59 |
--------------------------------------------------------------------------------
/dev/src/components/panels/tools/About.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import appVersion from '../../../appVersion';
3 | ---
4 |
5 |
12 |
--------------------------------------------------------------------------------
/dev/src/components/panels/tools/AppLog.astro:
--------------------------------------------------------------------------------
1 |
14 |
--------------------------------------------------------------------------------
/dev/src/components/panels/tools/AppSettings.astro:
--------------------------------------------------------------------------------
1 |
126 |
--------------------------------------------------------------------------------
/dev/src/components/panels/tools/ManageFiles.astro:
--------------------------------------------------------------------------------
1 |
2 |
48 |
49 |
--------------------------------------------------------------------------------
/dev/src/components/panels/tools/RemoteInfos.astro:
--------------------------------------------------------------------------------
1 |
2 |
21 |
22 |
--------------------------------------------------------------------------------
/dev/src/components/screens/LoadingScreen.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import AppLog from '../panels/tools/AppLog.astro';
3 | ---
4 |
5 |
6 |
7 |
8 |
9 | … …
10 |
11 |
12 |
13 | ⚠️ ⚠️
14 |
15 |
16 |
26 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/dev/src/components/screens/MainScreen.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import MobileMenu from '../MobileMenu.astro';
3 | import LightBox from '../LightBox.astro';
4 |
5 | import ActorPanel from '../panels/ActorPanel.astro';
6 | import FiltersPanel from '../panels/FiltersPanel.astro';
7 | import HeaderPanel from '../panels/HeaderPanel.astro';
8 | import PostsPanel from '../panels/PostsPanel.astro';
9 | import ConversationPanel from '../panels/ConversationPanel.astro';
10 | import TagsPanel from '../panels/TagsPanel.astro';
11 | import ToolsPanel from '../panels/ToolsPanel.astro';
12 | ---
13 |
14 |
21 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/dev/src/components/screens/WelcomeScreen.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import AppSettings from '../panels/tools/AppSettings.astro';
3 | import AppLog from '../panels/tools/AppLog.astro';
4 | import About from '../panels/tools/About.astro';
5 | ---
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
22 |
23 |
26 |
27 |
28 |
32 |
33 |
--------------------------------------------------------------------------------
/dev/src/css/_colors.scss:
--------------------------------------------------------------------------------
1 | // tint for the app color scheme
2 | $hue: 30; // === 59.17 in oklch
3 |
4 | /*********************************/
5 | /* global background/text colors */
6 | /*********************************/
7 |
8 | $bg0: #fff; // post content
9 | $bg1: hsl($hue, 12%, 95%); // post, incl. conversation
10 | $bg2: hsl($hue, 12%, 90%); // post attachment/meta/raw
11 | $bg3: hsl($hue, 15%, 89%); // posts header
12 | $bg4: hsl($hue, 17.5%, 84%); // filters/tags/tools panels
13 | $bg5: hsl($hue, 25%, 95%); // tools > ul
14 | $bg6: hsl($hue, 17.5%, 78%); // filters > reset button
15 |
16 | $fg0: #000; // post content
17 | $fg1: hsl($hue, 10%, 20%); // body text; some SVG icons (panel close)
18 | $fg2: hsl($hue, 10%, 30%); // headings (h1, h2...)
19 | $fg-inv: #fff; // misc highlighted text
20 |
21 | // dark theme
22 | $dark-bg0: hsl($hue, 6%, 20%);
23 | $dark-bg1: hsl($hue, 8%, 13%);
24 | $dark-bg2: hsl($hue, 10%, 10%);
25 | $dark-bg3: hsl($hue, 12%, 8%);
26 | $dark-bg4: hsl($hue, 12%, 8%);
27 | $dark-bg5: rgba(255, 255, 255, 0.07);
28 | $dark-bg6: hsl($hue, 12%, 6.5%);
29 | $dark-fg0: hsl($hue, 5%, 95%);
30 | $dark-fg1: hsl($hue, 10%, 80%);
31 | $dark-fg2: hsl($hue, 15%, 50%);
32 | $dark-fg-inv: #000;
33 |
34 | /***************/
35 | /* actor panel */
36 | /***************/
37 |
38 | $actor-bg0: hsl($hue, 37.5%, 11%); // bookmarks
39 | $actor-bg1: hsl($hue, 32.5%, 15%); // favorites
40 | $actor-bg2: hsl($hue, 25%, 20%); // actor raw data
41 | $actor-bg3: hsl($hue, 25%, 25%); // actor panel
42 | $actor-bg4: hsl($hue, 22.5%, 40%); // actor summary
43 |
44 | $actor-fg0: #fff;
45 | $actor-fg1: hsl($hue, 40%, 80%); // actor texts
46 | $actor-tabs-bg: hsl($hue, 12%, 17%);
47 |
48 | // OKLCH variants (multiple archives opened)
49 | $actor-bg0-ok: oklch(21% 2% var(--actor-hue));
50 | $actor-bg1-ok: oklch(24% 2% var(--actor-hue));
51 | $actor-bg2-ok: oklch(27% 2% var(--actor-hue));
52 | $actor-bg3-ok: oklch(30% 2% var(--actor-hue));
53 | $actor-bg4-ok: oklch(40% 10% var(--actor-hue));
54 | $actor-fg1-ok: oklch(92% 8% var(--actor-hue));
55 |
56 | $actor-accent: hsl($hue, 75%, 50%);
57 | $actor-accent-ok: oklch(80% 25% var(--actor-hue));
58 | $actor-accent2-ok: oklch(60% 20% var(--actor-hue)); // outline; color code on post
59 |
60 | // dark theme
61 | $dark-actor-bg0: hsl($hue, 32%, 2%);
62 | $dark-actor-bg1: hsl($hue, 30%, 4%);
63 | $dark-actor-bg2: hsl($hue, 27%, 6%);
64 | $dark-actor-bg3: hsl($hue, 25%, 8%);
65 | $dark-actor-bg4: hsl($hue, 20%, 15%);
66 | $dark-actor-fg0: $dark-fg0;
67 | $dark-actor-fg1: hsl($hue, 65%, 65%);
68 | $dark-actor-tabs-bg: hsl($hue, 20%, 8%);
69 | $dark-actor-bg0-ok: oklch(11% 1% var(--actor-hue));
70 | $dark-actor-bg1-ok: oklch(14% 1% var(--actor-hue));
71 | $dark-actor-bg2-ok: oklch(17% 1% var(--actor-hue));
72 | $dark-actor-bg3-ok: oklch(20% 1% var(--actor-hue));
73 | $dark-actor-bg4-ok: oklch(30% 6% var(--actor-hue));
74 | $dark-actor-fg1-ok: oklch(85% 25% var(--actor-hue));
75 | $dark-actor-accent: hsl($hue, 75%, 50%);
76 | $dark-actor-accent-ok: oklch(80% 25% var(--actor-hue));
77 | $dark-actor-accent2-ok: oklch(60% 20% var(--actor-hue));
78 |
79 | /***************/
80 | /* mobile menu */
81 | /***************/
82 |
83 | $menu-bg: hsl($hue, 25%, 20%);
84 | $menu-fg: hsl($hue, 40%, 80%);
85 | $menu-fg-active: #fff;
86 | $menu-icon: #fff;
87 | $menu-filter-active: hsl($hue, 75%, 50%);
88 |
89 | $menu-backdrop: rgba(0, 0, 0, 0.5);
90 |
91 | $panel-close: hsl($hue, 10%, 20%);
92 | $panel-close-hover: hsl($hue, 75%, 38%);
93 |
94 | // dark theme
95 | $dark-menu-bg: hsl($hue, 25%, 5%);
96 | $dark-menu-fg: hsl($hue, 40%, 80%);
97 | $dark-menu-fg-active: #fff;
98 | $dark-menu-icon: #fff;
99 | $dark-menu-filter-active: hsl($hue, 75%, 50%);
100 | $dark-menu-backdrop: rgba(0, 0, 0, 0.5);
101 | $dark-panel-close: hsl($hue, 5%, 90%);
102 | $dark-panel-close-hover: hsl($hue, 0%, 100%);
103 |
104 | /**********/
105 | /* accent */
106 | /**********/
107 |
108 | $accent: hsl($hue, 100%, 30%); // links, active elements
109 | $accent-dark: hsl($hue, 100%, 26%); // counters
110 | $accent-light: hsl($hue, 75%, 38%); // focusable elements outline; main header counter
111 | $accent-light2: hsl($hue, 75%, 50%); // overlay hover/focus icons (SVG); active filter indicator (mobile menu)
112 | $accent-light3: hsl($hue, 75%, 75%); // hover effect on images
113 |
114 | // dark theme
115 | $dark-accent: hsl($hue, 70%, 65%);
116 | $dark-accent-dark: hsl($hue, 55%, 50%);
117 | $dark-accent-light: hsl($hue, 75%, 60%);
118 | $dark-accent-light2: hsl($hue, 75%, 68%);
119 | $dark-accent-light3: hsl($hue, 75%, 75%);
120 |
121 | /***********/
122 | /* overlay */
123 | /***********/
124 |
125 | $overlay-icon: #fff;
126 | $overlay-icon-hover: #fff;
127 | $overlay-backdrop: rgba(0, 0, 0, 0.75);
128 |
129 | // dark theme
130 | $dark-overlay-icon: #fff;
131 | $dark-overlay-icon-hover: #fff;
132 | $dark-overlay-backdrop: rgba(0, 0, 0, 0.9);
133 |
134 | /*****************/
135 | /* form elements */
136 | /*****************/
137 |
138 | $bg-input: rgba(255, 255, 255, 0.35);
139 | $bg-input-hover: rgba(255, 255, 255, 0.65);
140 | $bg-input-focus: rgba(255, 255, 255, 1);
141 |
142 | $bg-button: rgba(255, 255, 255, 1);
143 | $bg-button-hover: hsl($hue, 50%, 95%);
144 | $button-svg: $fg1;
145 | $button-svg-hover: hsl($hue, 75%, 50%);
146 | $button-svg-focus: $button-svg-hover;
147 | $button-svg-active: $button-svg-hover;
148 | $input-separator: rgba(0, 0, 0, 0.2);
149 |
150 | $fg-button-focus: $accent;
151 | $fg-button-active: $accent;
152 |
153 | // dark theme
154 | $dark-bg-input: rgba(255, 255, 255, 0.10);
155 | $dark-bg-input-hover: rgba(255, 255, 255, 0.15);
156 | $dark-bg-input-focus: rgba(255, 255, 255, 0.25);
157 | $dark-bg-button: hsl($hue, 25%, 20%);
158 | $dark-bg-button-hover: hsl($hue, 25%, 30%);
159 | $dark-button-svg: $dark-fg1;
160 | $dark-button-svg-hover: hsl($hue, 65%, 65%);
161 | $dark-button-svg-focus: $dark-button-svg-hover;
162 | $dark-button-svg-active: $dark-button-svg-hover;
163 | $dark-input-separator: rgba(255, 255, 255, 0.15);
164 | $dark-fg-button-focus: $dark-accent;
165 | $dark-fg-button-active: $dark-accent;
166 |
167 | /*********/
168 | /* polls */
169 | /*********/
170 |
171 | $poll-bg: rgba($bg0, 85%);
172 | $poll-pc-bg: hsl($hue, 50%, 85%);
173 | $poll-pc-bg2: $bg1; // border around the percentage
174 |
175 | $dark-poll-bg: rgba($dark-bg0, 65%);
176 | $dark-poll-pc-bg: hsl($hue, 30%, 25%, .5);
177 | $dark-poll-pc-bg2: $dark-bg1;
178 |
179 | /********/
180 | /* misc */
181 | /********/
182 |
183 | $posts-count: $accent-light;
184 | $selection-text: #fff;
185 | $selection-bg: $accent-light;
186 | $stripe1: $accent;
187 | $stripe2: $accent-dark;
188 | $stripe-fg: $fg-inv;
189 | $private-post-bg: hsl($hue, 50%, 96%);
190 | $private-post-border: $accent-light2;
191 |
192 | // dark theme
193 | $dark-posts-count: $dark-fg0;
194 | $dark-selection-text: #fff;
195 | $dark-selection-bg: hsl($hue, 75%, 40%);
196 | $dark-stripe1: hsl($hue, 75%, 25%);
197 | $dark-stripe2: hsl($hue, 75%, 30%);
198 | $dark-stripe-fg: $dark-fg0;
199 | $dark-private-post-bg: $dark-bg2;
200 | $dark-private-post-border: $dark-accent-dark;
201 |
202 |
--------------------------------------------------------------------------------
/dev/src/css/_conversation.scss:
--------------------------------------------------------------------------------
1 | .conversation {
2 | position: fixed;
3 | left: 0;
4 | right: 0;
5 | top: 0;
6 | bottom: 0;
7 | z-index: 5;
8 | background: var(--bg1);
9 |
10 | display: flex;
11 | flex-direction: column;
12 | align-content: center;
13 | justify-content: flex-start;
14 |
15 | .conversation-close {
16 | z-index: 2;
17 | }
18 |
19 | .post-has-conversation {
20 | display: none !important;
21 | }
22 | }
23 |
24 | .conversation-header {
25 | position: sticky;
26 | top: 0;
27 | z-index: 5;
28 | padding: 1rem 3rem;
29 | text-align: center;
30 | background: var(--bg2);
31 | .count {
32 | font-size: 0.75em;
33 | margin-left: 0.4em;
34 | font-weight: normal;
35 | strong {
36 | color: var(--accent);
37 | font-weight: inherit;
38 | }
39 | }
40 | }
41 |
42 | .conversation-wrapper {
43 | padding: 1rem 0;
44 | }
45 |
46 | .conversation-help-wrapper {
47 | margin-top: auto;
48 | padding: 1rem 1rem 4.5rem;
49 | text-align: center;
50 | font-size: 0.85em;
51 | opacity: 0.6;
52 |
53 | &:hover,
54 | &:focus-within,
55 | &:has(details[open]) {
56 | opacity: 1;
57 | }
58 | }
59 | .conversation-help {
60 | max-width: 80ch;
61 | margin: 0 auto;
62 | }
63 | .conversation-help-summary {
64 | justify-content: center;
65 | gap: 0.35rem;
66 | }
67 | .conversation-help-content {
68 | margin-top: 0.5rem;
69 | padding: 0.5rem 1rem;
70 | background: var(--bg2);
71 | }
72 |
--------------------------------------------------------------------------------
/dev/src/css/_filters.scss:
--------------------------------------------------------------------------------
1 | .toots-filters {
2 | height: 100dvh;
3 | max-height: 100dvh;
4 | padding: 1rem 1rem 2em;
5 | padding-bottom: 0;
6 | background-color: var(--bg4);
7 | overflow-y: auto;
8 | container-type: inline-size;
9 | user-select: none;
10 |
11 | h2 {
12 | margin: 0 0 2rem;
13 | }
14 |
15 | .section-attachments,
16 | .section-must-contain {
17 | @container (width >= 350px) {
18 | display: flex;
19 | flex-wrap: wrap;
20 |
21 | .form-items-section-title {
22 | flex: 0 0 100%;
23 | }
24 | .form-items-group {
25 | margin-bottom: 0;
26 | flex: 0 0 50%;
27 |
28 | // this is to keep the vertical alignement with the
29 | // language group (below) in case it uses 2 columns
30 | padding-right: 2rem;
31 | position: relative;
32 | &::before {
33 | content: "";
34 | position: absolute;
35 | right: 1rem;
36 | top: 0;
37 | bottom: 0;
38 | width: 0;
39 | border-left: 1px solid var(--input-separator);
40 | }
41 | & + .form-items-group {
42 | padding-right: 0;
43 | &::before {
44 | content: none;
45 | }
46 | }
47 | }
48 | }
49 | }
50 |
51 | .section-datetime {
52 | .form-items-group {
53 | display: grid;
54 | grid-template-columns: max-content 1fr;
55 | grid-template-areas: "label input";
56 | }
57 | .form-item {
58 | grid-column: 1 / 3;
59 | display: grid;
60 | grid-template-columns: subgrid;
61 | }
62 | }
63 |
64 | .section-langs {
65 | .form-items-group:has(.form-item:nth-child(5)) {
66 | // 5 means 4 "form-item" + 1 template element (hidden), so 4 languages
67 | columns: 2;
68 | column-gap: 0;
69 |
70 | .form-item {
71 | margin-right: 1rem;
72 | }
73 | }
74 | }
75 | }
76 |
77 | .toots-filters-actions {
78 | margin: 0 -1rem;
79 | padding: 1rem;
80 |
81 | display: flex;
82 | gap: 1rem;
83 | justify-content: space-between;
84 |
85 | position: sticky;
86 | bottom: 0;
87 | z-index: 1;
88 | background-color: var(--bg6);
89 |
90 | .reset:not(:disabled) {
91 | box-shadow: 0 0 0 0.25rem var(--accent);
92 | }
93 | }
94 |
95 | .toots-filters-reset {
96 | margin-left: auto;
97 | }
98 |
--------------------------------------------------------------------------------
/dev/src/css/_forms.scss:
--------------------------------------------------------------------------------
1 | .form-items-section {
2 | margin: 0 0 1.5rem;
3 | }
4 | .form-items-section-title {
5 | margin: 0 0 0.75rem;
6 | }
7 | .form-items-group {
8 | margin: 0 0 0.75rem;
9 | &:last-child {
10 | margin-bottom: 0;
11 | }
12 | }
13 |
14 | .section-text {
15 | display: grid;
16 | grid-template-columns: min-content 1fr;
17 | gap: 0.35rem;
18 | .form-item {
19 | margin-bottom: 0;
20 | grid-column: span 2;
21 | display: grid;
22 | grid-template-columns: subgrid;
23 | }
24 | @container (width < 260px) {
25 | display: block;
26 | .form-item label {
27 | margin: 0.5rem 0 0.25rem;
28 | }
29 | }
30 | }
31 |
32 | .form-item {
33 | margin-bottom: 0.2rem;
34 |
35 | label {
36 | display: block;
37 | padding: 0.2rem 0.3rem;
38 | font-size: 0.85em;
39 | border-radius: 0.2rem;
40 | }
41 |
42 | &.sep-above {
43 | margin-top: 1rem;
44 | }
45 |
46 | &.text {
47 | input {
48 | display: block;
49 | width: 100%;
50 | }
51 | }
52 |
53 | &.number {
54 | display: flex;
55 | label {
56 | flex-grow: 1;
57 | margin-right: 0.35rem;
58 | }
59 | input {
60 | display: block;
61 | width: 7ch;
62 | text-align: left;
63 | }
64 | }
65 |
66 | &.select {
67 | display: flex;
68 | label {
69 | flex-grow: 1;
70 | margin-right: 0.35rem;
71 | }
72 | select {
73 | display: block;
74 | width: 20ch;
75 | }
76 | }
77 |
78 | &.checkbox {
79 | .checkbox-label {
80 | position: relative;
81 | padding-left: 1.7rem;
82 | background: transparent url("../svg/dark/checkbox-unchecked.svg") no-repeat 2px 2px / auto 18.4px scroll;
83 |
84 | &:has(input:checked) {
85 | background-image: url("../svg/dark/checkbox-checked.svg");
86 | }
87 | }
88 | input[type="checkbox"] {
89 | position: absolute;
90 | left: 1px;
91 | top: 1px;
92 | z-index: 1;
93 | opacity: 0;
94 | }
95 |
96 | @media (forced-colors: active) {
97 | .checkbox-label {
98 | background-image: none !important;
99 | }
100 | input[type="checkbox"] {
101 | left: 3px;
102 | top: 4px;
103 | opacity: 1;
104 | outline: none !important;
105 | }
106 | }
107 | }
108 |
109 | &.datetime {
110 | display: flex;
111 | gap: 0.35rem;
112 | .input-wrapper {
113 | input {
114 | display: block;
115 | width: 100%;
116 | }
117 | }
118 | }
119 |
120 | &.activity {
121 | display: flex;
122 | gap: 0.35rem;
123 | .checkbox-label {
124 | flex-grow: 1;
125 | }
126 | }
127 |
128 | &:hover label,
129 | &:focus-visible label,
130 | &:focus-within label {
131 | background-color: rgba(255, 255, 255, 0.35);
132 | }
133 |
134 | &.active label {
135 | color: var(--fg-inv);
136 | opacity: 1;
137 | background-color: var(--accent);
138 | @media (forced-colors: active) {
139 | outline: 1px solid Highlight;
140 | }
141 | }
142 | }
143 |
144 | .form-item-description {
145 | display: block;
146 | font-size: 0.95em;
147 | margin-top: 0.3rem;
148 | opacity: 0.7;
149 | }
150 |
151 | /********************/
152 | /* THEME VARIATIONS */
153 | /********************/
154 |
155 | // light
156 |
157 | @media (prefers-color-scheme: light) {
158 | .form-item.checkbox .checkbox-label {
159 | background-image: url("../svg/dark/checkbox-unchecked.svg");
160 | }
161 | .form-item.checkbox .checkbox-label:has(input:checked) {
162 | background-image: url("../svg/dark/checkbox-checked.svg");
163 | }
164 | .form-item:hover label,
165 | .form-item:focus-visible label,
166 | .form-item:focus-within label {
167 | background-color: rgba(255, 255, 255, 0.35);
168 | }
169 | .form-item.active label {
170 | background-color: var(--accent);
171 | }
172 | }
173 | html.light {
174 | .form-item.checkbox .checkbox-label {
175 | background-image: url("../svg/dark/checkbox-unchecked.svg");
176 | }
177 | .form-item.checkbox .checkbox-label:has(input:checked) {
178 | background-image: url("../svg/dark/checkbox-checked.svg");
179 | }
180 | .form-item:hover label,
181 | .form-item:focus-visible label,
182 | .form-item:focus-within label {
183 | background-color: rgba(255, 255, 255, 0.35);
184 | }
185 | .form-item.active label {
186 | background-color: var(--accent);
187 | }
188 | }
189 |
190 | // dark
191 |
192 | @media (prefers-color-scheme: dark) {
193 | .form-item.checkbox .checkbox-label {
194 | background-image: url("../svg/light/checkbox-unchecked.svg");
195 | }
196 | .form-item.checkbox .checkbox-label:has(input:checked) {
197 | background-image: url("../svg/light/checkbox-checked.svg");
198 | }
199 | .form-item:hover label,
200 | .form-item:focus-visible label,
201 | .form-item:focus-within label {
202 | background-color: rgba(255, 255, 255, 0.1);
203 | }
204 | .form-item.active label {
205 | background-color: var(--accent-light);
206 | }
207 | }
208 | html.dark {
209 | .form-item.checkbox .checkbox-label {
210 | background-image: url("../svg/light/checkbox-unchecked.svg");
211 | }
212 | .form-item.checkbox .checkbox-label:has(input:checked) {
213 | background-image: url("../svg/light/checkbox-checked.svg");
214 | }
215 | .form-item:hover label,
216 | .form-item:focus-visible label,
217 | .form-item:focus-within label {
218 | background-color: rgba(255, 255, 255, 0.1);
219 | }
220 | .form-item.active label {
221 | background-color: var(--accent-light);
222 | }
223 | }
224 |
--------------------------------------------------------------------------------
/dev/src/css/_global.scss:
--------------------------------------------------------------------------------
1 | @use "mixins";
2 | @use "colors-mixins";
3 |
4 | /* mini-reset */
5 | *,
6 | *::before,
7 | *::after {
8 | box-sizing: border-box;
9 | margin: 0;
10 | padding: 0;
11 | }
12 |
13 | /* anti-mini-reset */
14 | .actor-summary,
15 | .toot-content-inner {
16 | * {
17 | all: revert;
18 | }
19 |
20 | /* anti-anti-mini-reset */
21 | & > *:first-child {
22 | margin-top: 0;
23 | }
24 | & > *:last-child {
25 | margin-bottom: 0;
26 | }
27 | a {
28 | color: var(--accent);
29 | }
30 | }
31 |
32 | /* color schemes */
33 |
34 | html {
35 | @include colors-mixins.theme-vars();
36 |
37 | @media (prefers-color-scheme: dark) {
38 | color-scheme: dark;
39 | @include colors-mixins.theme-vars("dark");
40 | }
41 | }
42 |
43 | html.light {
44 | color-scheme: light;
45 | @include colors-mixins.theme-vars();
46 | }
47 |
48 | html.dark {
49 | color-scheme: dark;
50 | @include colors-mixins.theme-vars("dark");
51 | }
52 |
53 | /* global rules */
54 |
55 | html {
56 | overscroll-behavior: none;
57 | }
58 |
59 | body {
60 | margin: 0;
61 | padding: 0;
62 | color: var(--fg1);
63 | font-size: 1rem;
64 | background: var(--bg2);
65 | font-family: sans-serif;
66 | overscroll-behavior: none;
67 | }
68 |
69 | h1,
70 | h2,
71 | h3,
72 | h4,
73 | h5,
74 | h6 {
75 | color: var(--fg2);
76 | }
77 |
78 | img {
79 | display: block;
80 | height: auto;
81 | max-width: 100%;
82 | }
83 |
84 | a,
85 | .main-welcome p label {
86 | color: var(--accent);
87 | }
88 |
89 | input,
90 | button,
91 | label.btn,
92 | select {
93 | padding: 0.3em 0.5em;
94 | font-family: inherit;
95 | border: none;
96 | @media (forced-colors: active) {
97 | border: 1px solid ButtonText;
98 | }
99 | }
100 |
101 | select {
102 | padding: 0.6em 1em;
103 | }
104 |
105 | button,
106 | label.btn,
107 | select {
108 | position: relative;
109 | top: 0;
110 | line-height: 24px;
111 | vertical-align: middle;
112 | border-radius: 5px;
113 | background: var(--bg-button);
114 | box-shadow: 0 0.15em 0 0 rgba(0, 0, 0, 0.35);
115 | &:hover {
116 | background: var(--bg-button-hover);
117 | }
118 | &:focus {
119 | color: var(--fg-button-focus);
120 | }
121 | &:active {
122 | top: 0.15em;
123 | color: var(--fg-button-active);
124 | box-shadow: 0 0 0 0 rgba(0, 0, 0, 0);
125 | }
126 | &:disabled {
127 | top: 0;
128 | box-shadow: none;
129 | color: inherit;
130 | opacity: 0.35;
131 | cursor: not-allowed;
132 | }
133 | }
134 |
135 | button,
136 | label.btn {
137 | &:not(:has(svg)) {
138 | padding: 0.7em 1em;
139 | }
140 |
141 | .btn-icon,
142 | .btn-label {
143 | display: inline-block;
144 | line-height: 24px;
145 | vertical-align: top;
146 | }
147 | .btn-label {
148 | padding: 0 0.5em;
149 | }
150 | .btn-icon {
151 | width: 24px;
152 | height: 24px;
153 | fill: var(--button-svg);
154 | @media (forced-colors: active) {
155 | fill: ButtonText;
156 | }
157 | }
158 |
159 | &:hover {
160 | .btn-icon {
161 | fill: var(--button-svg-hover);
162 | @media (forced-colors: active) {
163 | fill: ButtonText;
164 | }
165 | }
166 | }
167 | &:focus {
168 | .btn-icon {
169 | fill: var(--button-svg-focus);
170 | @media (forced-colors: active) {
171 | fill: ButtonText;
172 | }
173 | }
174 | }
175 | &:active {
176 | .btn-icon {
177 | fill: var(--button-svg-active);
178 | @media (forced-colors: active) {
179 | fill: ButtonText;
180 | }
181 | }
182 | }
183 | &:disabled {
184 | .btn-icon {
185 | fill: var(--button-svg);
186 | @media (forced-colors: active) {
187 | fill: GrayText;
188 | }
189 | }
190 | }
191 | }
192 |
193 | input {
194 | color: inherit;
195 | background-color: var(--bg-input);
196 |
197 | &:hover {
198 | background-color: var(--bg-input-hover);
199 | }
200 | &:focus,
201 | &:focus-visible {
202 | background-color: var(--bg-input-focus);
203 | }
204 | }
205 |
206 | input[type="number"] {
207 | width: 6ch;
208 | text-align: center;
209 | -moz-appearance: textfield;
210 | appearance: textfield;
211 | }
212 | input[type="number"]::-webkit-inner-spin-button,
213 | input[type="number"]::-webkit-outer-spin-button {
214 | -webkit-appearance: none;
215 | margin: 0;
216 | }
217 |
218 | details {
219 | summary {
220 | cursor: pointer;
221 | user-select: none;
222 | display: flex;
223 | gap: 0.75rem;
224 | align-items: center;
225 | .summary-icon {
226 | min-width: 1.3rem;
227 | text-align: center;
228 | svg {
229 | height: 1.3em;
230 | aspect-ratio: 1;
231 | fill: currentColor;
232 | }
233 | }
234 | .summary-label {
235 | }
236 | }
237 | pre,
238 | textarea {
239 | white-space: pre-wrap;
240 | word-break: break-all;
241 | font-size: 0.8em;
242 | color: inherit;
243 | }
244 | textarea {
245 | display: block;
246 | width: 100%;
247 | resize: vertical;
248 | border: none;
249 | background: transparent;
250 | min-height: 20em;
251 | min-height: 15.5lh;
252 |
253 | @supports (field-sizing: content) {
254 | min-height: 0;
255 | field-sizing: content;
256 | }
257 | }
258 | }
259 |
260 | ::selection {
261 | color: var(--selection-text);
262 | background-color: var(--selection-bg);
263 | }
264 |
265 | // focusable elements
266 | *:focus-visible,
267 | .tags-group button:focus-visible div,
268 | .form-item:has([type="checkbox"]:focus-visible) {
269 | text-decoration: none;
270 | outline: 2px solid var(--accent-light);
271 | outline-offset: 2px;
272 | border-radius: 2px;
273 | position: relative;
274 | z-index: 1;
275 | }
276 | *:focus-visible .mobile-menu-panel {
277 | outline-offset: -4px;
278 | border-radius: 6px;
279 | }
280 |
281 | .visually-hidden:not(:focus):not(:active) {
282 | @include mixins.visually-hidden();
283 | }
284 |
285 | .nojs {
286 | margin: 5dvh 10dvw;
287 | padding: 2em;
288 | color: #fff;
289 | text-align: center;
290 | font-weight: bold;
291 | border-radius: 0.5rem;
292 | background-color: #600;
293 | }
294 |
295 | @media (forced-colors: active) {
296 | details {
297 | summary {
298 | color: ButtonText !important;
299 | svg {
300 | fill: ButtonText !important;
301 | }
302 | }
303 | }
304 | .tags-group button * {
305 | color: ButtonText !important;
306 | }
307 | }
308 |
--------------------------------------------------------------------------------
/dev/src/css/_layout.scss:
--------------------------------------------------------------------------------
1 | @use "mixins";
2 |
3 | html {
4 | width: 100dvw;
5 | height: 100dvh;
6 | overflow: hidden;
7 | }
8 |
9 | .main-section-inner {
10 | position: relative;
11 | z-index: 1;
12 | height: 100dvh;
13 | max-width: 100dvw;
14 | overflow: hidden;
15 | display: grid;
16 | grid-template-areas:
17 | "actor filters header tags"
18 | "actor filters toots tags";
19 | grid-template-columns: min(25%, 600px) min(20%, 400px) 1fr min(20%, 400px);
20 | grid-template-rows: min-content 1fr;
21 | }
22 |
23 | .actor {
24 | grid-area: actor;
25 | }
26 | .toots-header {
27 | grid-area: header;
28 | }
29 | .toots-filters {
30 | grid-area: filters;
31 | }
32 | .toots {
33 | grid-area: toots;
34 | }
35 | .toots-tags {
36 | grid-area: tags;
37 | }
38 | .mobile-menu {
39 | display: none;
40 | }
41 |
42 | .toots-header {
43 | display: flex;
44 | flex-wrap: wrap;
45 | justify-content: space-between;
46 |
47 | white-space: nowrap;
48 | overflow: hidden;
49 | background-color: var(--bg3);
50 |
51 | h2 {
52 | padding: 1rem 1rem 0;
53 | .order {
54 | font-weight: normal;
55 | &::before {
56 | content: "- ";
57 | }
58 | }
59 | }
60 |
61 | .nb {
62 | color: var(--posts-count);
63 | }
64 | .open-tools {
65 | padding: 1rem 1rem 0;
66 | }
67 | .paging {
68 | flex: 0 0 100%;
69 | }
70 | }
71 |
72 | .toots,
73 | .conversation {
74 | position: relative;
75 | background: var(--bg1);
76 | overflow: auto;
77 | container-type: inline-size;
78 | @media (prefers-reduced-motion: no-preference) {
79 | scroll-behavior: smooth;
80 | }
81 | }
82 |
83 | .paging {
84 | display: flex;
85 | justify-content: space-between;
86 | align-items: center;
87 | gap: 0.5rem;
88 | padding: 1rem;
89 | .direction-back,
90 | .direction-fwd {
91 | display: flex;
92 | flex-wrap: wrap;
93 | gap: 0.25rem;
94 | }
95 | .direction-back {
96 | justify-content: flex-start;
97 | }
98 | .direction-fwd {
99 | justify-content: flex-end;
100 | }
101 | }
102 |
103 | // bottom paging
104 | .toots .paging {
105 | padding-bottom: 2rem;
106 | // -> align with padding-bottom on .toots-filters and .toots-tags (side panels)
107 |
108 | @media (width < 400px) {
109 | .direction-back,
110 | .direction-fwd {
111 | .btn-label {
112 | @include mixins.visually-hidden();
113 | }
114 | }
115 | }
116 | }
117 |
118 | // top paging
119 | .toots-header {
120 | container-type: inline-size;
121 |
122 | input {
123 | font-size: inherit;
124 | text-align: right;
125 | field-sizing: content;
126 | min-width: 3ch;
127 | max-width: 10ch;
128 | }
129 |
130 | .paging {
131 | clear: both;
132 | flex-wrap: wrap;
133 | .paging-options {
134 | order: 2;
135 | user-select: none;
136 | }
137 | .direction-back {
138 | order: 1;
139 | }
140 | .direction-fwd {
141 | order: 3;
142 | }
143 | }
144 |
145 | @container (width < 475px) {
146 | .paging-label {
147 | display: none;
148 | }
149 | } // ! @container
150 |
151 | @container (width < 650px) {
152 | .paging {
153 | .direction-back,
154 | .direction-fwd {
155 | .btn-label {
156 | @include mixins.visually-hidden();
157 | }
158 | }
159 | }
160 | } // ! @container
161 | }
162 |
163 | @media screen and (width >= 1400px) {
164 | .combine-panels {
165 | .toots-header {
166 | display: grid;
167 | grid-template-columns: 1fr auto 1fr;
168 | grid-template-areas: "area1 area2 area3";
169 |
170 | h2 {
171 | grid-area: area1;
172 | }
173 |
174 | .paging {
175 | grid-area: area2;
176 | flex-basis: auto;
177 | flex-grow: 1;
178 | .paging-options {
179 | margin: 0 1rem;
180 | }
181 | }
182 | }
183 | .toots {
184 | padding-top: 2.5rem;
185 | }
186 | }
187 | }
188 |
--------------------------------------------------------------------------------
/dev/src/css/_mixins.scss:
--------------------------------------------------------------------------------
1 | @mixin visually-hidden() {
2 | width: 1px;
3 | height: 1px;
4 | overflow: hidden;
5 | clip: rect(0 0 0 0);
6 | clip-path: inset(50%);
7 | position: absolute;
8 | white-space: nowrap;
9 | }
10 |
11 | @mixin box-shadow-inner() {
12 | box-shadow: 0 0.15rem 0.3rem -0.2rem rgba(0, 0, 0, 0.35) inset;
13 | }
14 | @mixin box-shadow-post() {
15 | box-shadow: 0 0.3rem 0.4rem -0.2rem rgba(0, 0, 0, 0.3);
16 | }
17 |
--------------------------------------------------------------------------------
/dev/src/css/_overlay.scss:
--------------------------------------------------------------------------------
1 | .overlay {
2 | position: fixed;
3 | left: 0;
4 | right: 0;
5 | top: 0;
6 | bottom: 0;
7 | z-index: 100;
8 | display: flex;
9 | align-items: center;
10 | justify-content: center;
11 | }
12 |
13 | .overlay-content {
14 | position: relative;
15 | z-index: 5;
16 | }
17 |
18 | .overlay-content,
19 | .overlay-content img {
20 | max-width: calc(100dvw - 88px);
21 | max-height: calc(100dvh - 44px);
22 | }
23 |
24 | .overlay-content img {
25 | box-shadow: 0 1rem 2.5rem -1rem rgba(0, 0, 0, 1);
26 | }
27 |
28 | .overlay-ui {
29 | position: absolute;
30 | left: 0;
31 | right: 0;
32 | top: 0;
33 | bottom: 0;
34 | z-index: 1;
35 |
36 | button {
37 | display: block;
38 | padding: 0;
39 | position: absolute;
40 | z-index: 2;
41 | cursor: pointer;
42 | border: none;
43 | background: transparent;
44 | box-shadow: none;
45 | svg {
46 | display: block;
47 | width: 36px;
48 | height: 36px;
49 | fill: var(--overlay-icon);
50 | }
51 | &:hover,
52 | &:focus,
53 | &:active {
54 | svg {
55 | fill: var(--overlay-icon-hover);
56 | }
57 | }
58 | }
59 | .viewer-close {
60 | top: 4px;
61 | right: 4px;
62 | svg {
63 | width: 24px;
64 | height: 24px;
65 | }
66 | }
67 | .viewer-next {
68 | right: 4px;
69 | top: calc(50% - 18px);
70 | }
71 | .viewer-prev {
72 | left: 4px;
73 | top: calc(50% - 18px);
74 | }
75 |
76 | .backdrop {
77 | position: absolute;
78 | left: 0;
79 | right: 0;
80 | top: 0;
81 | bottom: 0;
82 | z-index: 1;
83 | background-color: var(--overlay-backdrop);
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/dev/src/css/_tags.scss:
--------------------------------------------------------------------------------
1 | $display-wide: 340px;
2 |
3 | .toots-tags {
4 | display: grid;
5 | grid-template-rows: min-content repeat(3, 1fr);
6 | height: 100vh;
7 | padding-bottom: 2rem;
8 | overflow: hidden;
9 | background-color: var(--bg4);
10 | container-type: inline-size;
11 |
12 | &.no-boosts {
13 | grid-template-rows: min-content repeat(2, 1fr);
14 | }
15 | }
16 |
17 | .tags-title {
18 | padding: 1rem;
19 | }
20 | .tags-group {
21 | position: relative;
22 | height: 100%;
23 | overflow: hidden;
24 | display: grid;
25 | padding: 0.5rem 1rem;
26 | grid-template-rows: min-content 1fr;
27 |
28 | ul {
29 | font-size: 0.85em;
30 | list-style: none;
31 | }
32 | li {
33 | list-style: none;
34 | margin: 0.3rem 4px;
35 | }
36 | ul button {
37 | all: unset;
38 | display: block;
39 | width: 100%;
40 | }
41 | button {
42 | div {
43 | display: flex;
44 | align-items: center;
45 | width: 100%;
46 | gap: 0.25rem;
47 | padding: 0.1rem 0.3rem;
48 | border-radius: 0.2rem;
49 | border: none;
50 | background: transparent;
51 | font-family: inherit;
52 | font-size: inherit;
53 | cursor: pointer;
54 |
55 | .count {
56 | color: var(--accent-dark);
57 | font-size: 0.85em;
58 | font-weight: bold;
59 |
60 | @container (width >= #{$display-wide}) {
61 | display: inline-block;
62 | min-width: 5ch;
63 | text-align: right;
64 | }
65 | }
66 | .name {
67 | flex-grow: 1;
68 | word-break: break-all;
69 | display: flex;
70 | flex-wrap: wrap;
71 | justify-content: space-between;
72 | align-items: center;
73 | }
74 | .domain {
75 | margin-left: auto;
76 | padding-left: 0.5em;
77 | font-size: 0.85em;
78 | font-weight: bold;
79 | letter-spacing: 0.02em;
80 | opacity: 0.75;
81 | }
82 | }
83 |
84 | &:hover {
85 | .name {
86 | text-decoration: underline;
87 | }
88 | }
89 | }
90 | .active button div {
91 | color: var(--fg-inv);
92 | background-color: var(--accent);
93 | .count {
94 | color: var(--fg-inv);
95 | }
96 | }
97 | }
98 |
99 | .tags-group-header {
100 | @container (width >= #{$display-wide}) {
101 | display: flex;
102 | justify-content: space-between;
103 | gap: 0.25rem 1rem;
104 | }
105 | h3 {
106 | white-space: nowrap;
107 | overflow: hidden;
108 | .count {
109 | color: var(--accent-dark);
110 | font-weight: normal;
111 | }
112 | }
113 | }
114 | .tags-group-filter {
115 | flex-grow: 1;
116 | position: relative;
117 | input {
118 | display: block;
119 | width: 100%;
120 | margin-top: 0.25rem;
121 | padding-right: 2rem;
122 | @container (width >= #{$display-wide}) {
123 | margin-top: 0;
124 | }
125 | }
126 | }
127 | .tags-group-filter-reset {
128 | position: absolute;
129 | right: 0;
130 | top: 0;
131 | bottom: 0;
132 | z-index: 2;
133 | padding: 0;
134 | aspect-ratio: 1;
135 | box-shadow: none;
136 | background-color: transparent;
137 | svg {
138 | width: 100%;
139 | aspect-ratio: 1;
140 | fill: currentColor;
141 | }
142 | }
143 |
144 | .tags-group-scroll {
145 | overflow: auto;
146 | margin-top: 0.5rem;
147 | padding-bottom: 2rem;
148 | }
149 |
150 | /********************/
151 | /* THEME VARIATIONS */
152 | /********************/
153 |
154 | // light
155 |
156 | @media (prefers-color-scheme: light) {
157 | .tags-group .active button div {
158 | background-color: var(--accent);
159 | }
160 | }
161 | html.light {
162 | .tags-group .active button div {
163 | background-color: var(--accent);
164 | }
165 | }
166 |
167 | // dark
168 |
169 | @media (prefers-color-scheme: dark) {
170 | .tags-group .active button div {
171 | background-color: var(--accent-light);
172 | }
173 | }
174 | html.dark {
175 | .tags-group .active button div {
176 | background-color: var(--accent-light);
177 | }
178 | }
179 |
--------------------------------------------------------------------------------
/dev/src/css/_tools.scss:
--------------------------------------------------------------------------------
1 | @use "mixins";
2 |
3 | .tools-section {
4 | h3 {
5 | margin: 0 0 1rem;
6 | }
7 | ul {
8 | list-style: none;
9 | font-size: 0.85em;
10 | margin: 1rem 0 0;
11 | padding: 0.5rem 1rem;
12 | background-color: var(--bg5);
13 | border-radius: 0.5rem;
14 | }
15 | li {
16 | list-style: none;
17 | margin: 0.5rem 0;
18 | }
19 | b,
20 | .name {
21 | word-break: break-all;
22 | }
23 | }
24 |
25 | .panel-tools {
26 | position: absolute;
27 | left: auto;
28 | right: calc(0px - min(25%, 600px));
29 | top: 0;
30 | bottom: 0;
31 | z-index: 4;
32 |
33 | width: min(25%, 600px);
34 | height: 100dvh;
35 | overflow-y: scroll;
36 | padding: 0;
37 |
38 | box-shadow: 0.75rem 0 1rem 0.75rem rgba(0, 0, 0, 0.35);
39 | background-color: var(--bg4);
40 | backdrop-filter: blur(20px);
41 |
42 | display: flex;
43 | flex-direction: column;
44 | justify-content: flex-start;
45 | container-type: inline-size;
46 |
47 | &[inert="true"] {
48 | box-shadow: none;
49 | }
50 |
51 | @media (prefers-reduced-motion: no-preference) {
52 | transition: right 0.2s ease-out;
53 | transition-property: right, box-shadow;
54 | scroll-behavior: smooth;
55 | }
56 |
57 | .menu-open-tools & {
58 | right: 0;
59 | }
60 |
61 | .panel-close {
62 | display: block !important;
63 | }
64 | .tools-section {
65 | padding: 1rem 1rem 2rem;
66 | &.app-log {
67 | padding-bottom: 0;
68 | }
69 | &.about {
70 | padding-top: 2rem;
71 | }
72 | }
73 | }
74 |
75 | .app-settings {
76 | h3 {
77 | user-select: none;
78 | }
79 | .form-item {
80 | margin-bottom: 0.5rem;
81 | }
82 | .form-items-section {
83 | margin: 0 0 2rem;
84 | }
85 | .form-items-section-title,
86 | .form-item.number,
87 | .form-item.checkbox {
88 | flex: 0 0 100%;
89 | }
90 | }
91 | .settings-layout-misc {
92 | display: flex;
93 | flex-wrap: wrap;
94 | justify-content: space-between;
95 | gap: 1rem;
96 | }
97 |
98 | .app-settings-default-panel {
99 | padding-left: 1.4rem;
100 | }
101 |
102 | .switch-lang {
103 | display: flex;
104 | gap: 1rem;
105 | line-height: 24px;
106 | label {
107 | display: block;
108 | width: 24px;
109 | height: 24px;
110 | padding: 0.3rem 0.5rem;
111 | svg {
112 | width: 24px;
113 | height: 24px;
114 | fill: var(--button-svg);
115 | @media (forced-colors: active) {
116 | fill: ButtonText;
117 | }
118 | }
119 | }
120 | }
121 |
122 | .switch-theme {
123 | &.theme-dark {
124 | .btn-label.dark {
125 | display: none;
126 | }
127 | }
128 | &.theme-light {
129 | .btn-label.light {
130 | display: none;
131 | }
132 | }
133 | }
134 |
135 | .settings-posts {
136 | .number label {
137 | padding-left: 1.7rem;
138 | }
139 | }
140 | .settings-posts {
141 | margin-bottom: 0;
142 | }
143 |
144 | .loaded-files-list {
145 | li:has(.date) {
146 | display: flex;
147 | flex-wrap: wrap;
148 | justify-content: space-between;
149 | }
150 | li + li {
151 | margin-top: 1rem;
152 | }
153 | .name {
154 | flex: 0 0 100%;
155 | color: var(--accent);
156 | }
157 | .author {
158 | margin-left: 0.5em;
159 | &::before {
160 | content: "(";
161 | }
162 | &::after {
163 | content: ")";
164 | }
165 | }
166 | .date + .author {
167 | margin: 0 1em;
168 | &::before,
169 | &::after {
170 | content: none;
171 | }
172 | }
173 |
174 | @media (forced-colors: none) {
175 | .multiple-files & li {
176 | padding-left: 0.75rem;
177 | position: relative;
178 | &::before {
179 | content: "";
180 | position: absolute;
181 | left: -0.125rem;
182 | top: 0;
183 | bottom: 0;
184 | z-index: 1;
185 | width: 0.25rem;
186 | border-radius: 0.25rem;
187 | background-color: var(--actor-accent-ok, transparent);
188 | }
189 | }
190 | }
191 | }
192 |
193 | .manage-files {
194 | margin-top: auto;
195 | }
196 |
197 | .manage-files-actions {
198 | display: flex;
199 | flex-wrap: wrap;
200 | gap: 1rem;
201 | margin-top: 1rem;
202 |
203 | .file-loader {
204 | @include mixins.visually-hidden();
205 | }
206 |
207 | .tip {
208 | flex: 0 0 100%;
209 | font-size: 0.8em;
210 | text-align: center;
211 | font-style: italic;
212 | opacity: 0.85;
213 | }
214 | }
215 |
216 | .app-log {
217 | ul {
218 | height: 20dvh;
219 | overflow-y: auto;
220 | }
221 |
222 | @media (forced-colors: none) {
223 | ul:has(.error, .warn) {
224 | li {
225 | padding-left: 0.75rem;
226 | }
227 | }
228 | li {
229 | position: relative;
230 | &.error,
231 | &.warn {
232 | padding-left: 0.75rem;
233 | &::before {
234 | content: "";
235 | position: absolute;
236 | left: -0.125rem;
237 | top: 0;
238 | bottom: 0;
239 | z-index: 1;
240 | width: 0.25rem;
241 | border-radius: 0.25rem;
242 | background-color: red;
243 | }
244 | }
245 | &.warn {
246 | &::before {
247 | background-color: orange;
248 | }
249 | }
250 | }
251 | }
252 |
253 | .msg {
254 | b {
255 | font-weight: normal;
256 | color: var(--accent);
257 | }
258 | }
259 | .time {
260 | opacity: 0.65;
261 | }
262 | }
263 |
264 | .about {
265 | font-size: 0.85em;
266 | text-align: center;
267 | }
268 |
--------------------------------------------------------------------------------
/dev/src/css/_welcome.scss:
--------------------------------------------------------------------------------
1 | @keyframes movingStripes {
2 | 0% {
3 | background-position: 0 0;
4 | }
5 | 100% {
6 | background-position: calc(sqrt(8) * -1rem) 0;
7 | }
8 | }
9 |
10 | .welcome,
11 | .loading {
12 | max-width: 80ch;
13 | height: 100dvh;
14 | margin: 0 auto;
15 | padding: 10dvh 2rem max(2dvh, 2rem);
16 | display: flex;
17 | flex-direction: column;
18 | justify-content: space-between;
19 | overflow: auto;
20 | line-height: 1.4;
21 | position: relative;
22 | z-index: 1;
23 | }
24 |
25 | .welcome {
26 | .app-log {
27 | h3 {
28 | display: none;
29 | }
30 | ul {
31 | height: auto;
32 | }
33 | li {
34 | display: none;
35 | &.error {
36 | display: block;
37 | }
38 | }
39 | }
40 | .tools {
41 | h3 {
42 | display: none;
43 | }
44 | }
45 | .app-settings {
46 | .form-items-section {
47 | display: none;
48 | }
49 | .settings-layout-misc {
50 | display: flex;
51 | }
52 | }
53 | .about {
54 | margin-top: 2rem;
55 | }
56 | }
57 |
58 | .intro {
59 | margin-bottom: 4dvh;
60 | h1 {
61 | margin-bottom: 7dvh;
62 | text-align: center;
63 | line-height: 1.1;
64 | .accronym {
65 | display: block;
66 | font-size: 0.8em;
67 | opacity: 0.7;
68 | }
69 | }
70 | p {
71 | margin: 1em 0;
72 | label {
73 | color: var(--accent);
74 | text-decoration: underline;
75 | cursor: pointer;
76 | @media (forced-colors: active) {
77 | color: ButtonText;
78 | }
79 | }
80 | }
81 | }
82 |
83 | .file-loader {
84 | display: block;
85 | width: 100%;
86 | height: 10rem;
87 | max-height: 70dvh;
88 | margin-top: 7dvh;
89 | padding: 1rem;
90 | border-radius: 0.5rem;
91 | background: var(--bg0);
92 | &:hover,
93 | &:focus {
94 | background: var(--bg0);
95 | outline: 2px solid var(--accent-light);
96 | outline-offset: 2px;
97 | }
98 | }
99 |
100 | .main-page.highlight-drag {
101 | &::before {
102 | content: "";
103 | position: absolute;
104 | left: 0;
105 | right: 0;
106 | top: 0;
107 | bottom: 0;
108 | z-index: -1;
109 |
110 | background: repeating-linear-gradient(
111 | 45deg,
112 | var(--stripe1),
113 | var(--stripe1) 1rem,
114 | var(--stripe2) 1rem,
115 | var(--stripe2) 2rem
116 | );
117 | background-size: 200% auto;
118 |
119 | @media (prefers-reduced-motion: no-preference) {
120 | animation: movingStripes 2s linear infinite;
121 | }
122 | }
123 | &::after {
124 | content: "";
125 | position: absolute;
126 | left: 1rem;
127 | right: 1rem;
128 | top: 1rem;
129 | bottom: 1rem;
130 | z-index: 0;
131 |
132 | background: var(--bg2);
133 | }
134 | }
135 |
136 | .loading {
137 | padding: 2rem;
138 | justify-content: center;
139 | }
140 |
141 | .loading-inner {
142 | }
143 | .loading-current {
144 | display: flex;
145 | gap: 1rem;
146 | flex-direction: column;
147 | justify-content: center;
148 |
149 | width: 100%;
150 | min-height: 10rem;
151 | max-height: 70dvh;
152 | padding: 1rem;
153 | color: var(--stripe-fg);
154 | text-align: center;
155 | border-radius: 0.5rem;
156 | background: var(--accent);
157 | background: repeating-linear-gradient(
158 | 45deg,
159 | var(--stripe1),
160 | var(--stripe1) 1rem,
161 | var(--stripe2) 1rem,
162 | var(--stripe2) 2rem
163 | );
164 | background-size: 200% auto;
165 |
166 | @media (prefers-reduced-motion: no-preference) {
167 | animation: movingStripes 2s linear infinite;
168 | }
169 | }
170 | .loading-txt {
171 | font-size: 2em;
172 | }
173 | .loading-filename {
174 | font-style: italic;
175 | overflow-wrap: anywhere;
176 | }
177 | .loading-next {
178 | margin-top: 1.75rem;
179 | text-align: center;
180 | font-size: 0.85em;
181 | ul {
182 | list-style: none;
183 | height: calc(4lh + (4 * 0.5rem));
184 | overflow: visible;
185 | }
186 | li {
187 | overflow-wrap: anywhere;
188 | line-height: 1.2;
189 | padding: 0.25rem 1rem;
190 | display: none;
191 | &.f1 {
192 | display: block;
193 | opacity: 1;
194 | }
195 | &.f2 {
196 | display: block;
197 | opacity: 0.75;
198 | }
199 | &.f3 {
200 | display: block;
201 | opacity: 0.5;
202 | }
203 | &.f4 {
204 | display: block;
205 | opacity: 0.25;
206 | }
207 | }
208 |
209 | & + .app-log-wrapper {
210 | margin-top: 1.75rem;
211 | h3 {
212 | display: none;
213 | }
214 | }
215 | }
216 |
--------------------------------------------------------------------------------
/dev/src/css/main.scss:
--------------------------------------------------------------------------------
1 | @use "colors";
2 | @use "colors-mixins";
3 | @use "mixins";
4 |
5 | @use "global";
6 | @use "forms";
7 | @use "layout";
8 | @use "welcome";
9 | @use "actor";
10 | @use "filters";
11 | @use "post";
12 | @use "conversation";
13 | @use "tags";
14 | @use "tools";
15 | @use "menu";
16 | @use "overlay";
17 |
--------------------------------------------------------------------------------
/dev/src/img/no-avatar-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/s427/MARL/f173ee599493b68b2d25cb47e9c09cea677dd974/dev/src/img/no-avatar-dark.png
--------------------------------------------------------------------------------
/dev/src/img/no-avatar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/s427/MARL/f173ee599493b68b2d25cb47e9c09cea677dd974/dev/src/img/no-avatar.png
--------------------------------------------------------------------------------
/dev/src/img/no-header-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/s427/MARL/f173ee599493b68b2d25cb47e9c09cea677dd974/dev/src/img/no-header-dark.png
--------------------------------------------------------------------------------
/dev/src/img/no-header.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/s427/MARL/f173ee599493b68b2d25cb47e9c09cea677dd974/dev/src/img/no-header.png
--------------------------------------------------------------------------------
/dev/src/pages/index.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import '../css/main.scss';
3 |
4 | import App from '../components/App.astro';
5 | import SvgSprites from '../components/SvgSprites.astro';
6 | import appVersion from '../appVersion';
7 | ---
8 |
9 |
10 |
11 |
12 |
13 | MARL - Mastodon Archive Reader Lite
14 |
15 |
16 |
17 |
18 |
19 |
20 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/dev/src/svg/dark/activity-like.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/dev/src/svg/dark/activity-share.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/dev/src/svg/dark/at.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/dev/src/svg/dark/calendar.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/dev/src/svg/dark/checkbox-checked.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/dev/src/svg/dark/checkbox-unchecked.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/dev/src/svg/dark/hashtag.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/dev/src/svg/dark/link.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/dev/src/svg/dark/open-in-new.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/dev/src/svg/dark/person.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/dev/src/svg/dark/type-boost.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/dev/src/svg/dark/type-post.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/dev/src/svg/dark/visibility-followers.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/dev/src/svg/dark/visibility-mentioned.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/dev/src/svg/dark/visibility-public.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/dev/src/svg/dark/visibility-unlisted.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/dev/src/svg/light/activity-like.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/dev/src/svg/light/activity-share.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/dev/src/svg/light/calendar.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/dev/src/svg/light/checkbox-checked.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/dev/src/svg/light/checkbox-unchecked.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/dev/src/svg/light/hashtag.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/dev/src/svg/light/link.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/dev/src/svg/light/menu-filters.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/dev/src/svg/light/menu-new.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/dev/src/svg/light/menu-profile.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/dev/src/svg/light/menu-tags.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/dev/src/svg/light/no-alt-text.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/dev/src/svg/light/open-in-new.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/dev/src/svg/light/person.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/dev/src/svg/light/type-boost.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/dev/src/svg/light/type-post.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/dev/src/svg/light/visibility-followers.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/dev/src/svg/light/visibility-mentioned.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/dev/src/svg/light/visibility-public.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/dev/src/svg/light/visibility-unlisted.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/dev/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "astro/tsconfigs/strict",
3 | "include": [".astro/types.d.ts", "**/*"],
4 | "exclude": ["dist"]
5 | }
6 |
--------------------------------------------------------------------------------
/dist/_astro/no-avatar-dark.DVWNt87i.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/s427/MARL/f173ee599493b68b2d25cb47e9c09cea677dd974/dist/_astro/no-avatar-dark.DVWNt87i.png
--------------------------------------------------------------------------------
/dist/_astro/no-avatar.DeffBVR2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/s427/MARL/f173ee599493b68b2d25cb47e9c09cea677dd974/dist/_astro/no-avatar.DeffBVR2.png
--------------------------------------------------------------------------------
/dist/_astro/no-header-dark.BP11rozj.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/s427/MARL/f173ee599493b68b2d25cb47e9c09cea677dd974/dist/_astro/no-header-dark.BP11rozj.png
--------------------------------------------------------------------------------
/dist/_astro/no-header.DKXzxkmt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/s427/MARL/f173ee599493b68b2d25cb47e9c09cea677dd974/dist/_astro/no-header.DKXzxkmt.png
--------------------------------------------------------------------------------
/dist/js/MARL/init.js:
--------------------------------------------------------------------------------
1 | loadScript("alpine-i18n"),loadScript("alpine"),document.addEventListener("alpine:init",(()=>{Alpine.store("files",filesStore),Alpine.store("lightbox",lightboxStore),Alpine.store("ui",uiStore),Alpine.store("userPrefs",userPrefsStore);const e=["Hi!","Hiya!","Hello there!","Good day!","Hullo!","Buongiorno!","Guten Tag!","Bonjour!","Oh hey!"];marlConsole(`MARL loaded. ${e[Math.floor(Math.random()*e.length)]} 😊`),checkMobileLayout(),setMarlMode(),loadCustomPrefs(),resetStores(),serverMode()?initServerMode():initLocalMode()})),document.addEventListener("alpine-i18n:ready",(function(){AlpineI18n.create("en",appStrings),AlpineI18n.fallbackLocale="en",setLang()}));
--------------------------------------------------------------------------------
/dist/js/MARL/libs.js:
--------------------------------------------------------------------------------
1 | const isFileProtocol="file:"===window.location.protocol,scripts={jszip:{src:"js/libs/jszip.min.js",integrity:"sha512-XMVd28F1oH/O71fzwBnV7HucLxVwtxf26XV8P4wPk26EDxuGZ91N8bsOttmnomcCD3CS5ZMRL50H0GgOHvegtg==",crossorigin:"anonymous",defer:!1},"alpine-i18n":{src:"js/libs/alpinejs-i18n.min.js",integrity:"sha256-o204NcFyHPFzboSC51fufMqFe2KJdQfSCl8AlvSZO/E=",crossorigin:"anonymous",defer:!0},alpine:{src:"js/libs/alpinejs.min.js",integrity:"sha512-FUaEyIgi9bspXaH6hUadCwBLxKwdH7CW24riiOqA5p8hTNR/RCLv9UpAILKwqs2AN5WtKB52CqbiePBei3qjKg==",crossorigin:"anonymous",defer:!0}};function loadScript(i){const s=scripts[i],n=document.createElement("script");n.src=s.src,isFileProtocol||(n.integrity=s.integrity,n.crossOrigin=s.crossorigin),s.defer&&(n.defer=!0),document.head.appendChild(n)}
--------------------------------------------------------------------------------
/dist/js/config.default.js:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | This file is used to determine whether MARL operates in LOCAL or SERVER mode.
4 | For more information, please read the "server-mode.md" documentation file.
5 |
6 | Important: please rename this file "config.js" if you want to use it.
7 |
8 | 1. local mode
9 | =============
10 |
11 | If the "servers" constant (below) is absent or empty, (or if this file is removed or renamed) MARL will work in LOCAL mode.
12 | Default value:
13 |
14 | const servers = [];
15 |
16 | 2. server mode
17 | ==============
18 |
19 | Use the "servers" constant below to indicate the path(s) where MARL can find your archive(s).
20 |
21 | If a path is specified, MARL will automatically switch to server mode.
22 |
23 | Relative paths are authorized. They should be relative to MARL (index.html), not to this config file.
24 |
25 | Example:
26 |
27 | const servers = [
28 | "../data/archive1/",
29 | "../data/archive2/",
30 | "https://my-other-server.net/some-other-path/",
31 | ];
32 |
33 | Each archive file must be already unpacked on the server. The "actor.json" and "outbox.json" files should be stored directly under the specified path.
34 |
35 | You may specify as many archives as you want. You can store them wherever you want. In case the archive is stored on a different server from MARL itself, make sure that the server has CORS enabled.
36 |
37 | */
38 |
39 | const servers = [];
40 |
41 | /*
42 |
43 | You can use the following object to change the default options for the app (those are the options found in the tools panel).
44 | Note: Users will still be able to change those options for themselves.
45 |
46 | const customPrefs = {};
47 |
48 | You can use the following properties. All properties are optional:
49 |
50 | - lang: the language for the UI
51 | possible values: "en" (default), "fr", "es"
52 | note: if no option is set, the app will attempt to use the same language as the user's browser (if available for the app)
53 |
54 | - theme: the theme for the app
55 | possible values: "light" (default), "dark"
56 | note: if no option is set, the app will follow the user preference (set in their browser or OS)
57 |
58 | - sortAsc: the order of the posts (chronological or not); true means "oldest posts first"
59 | possible values: true (default), false
60 |
61 | - pageSize: the number of posts per page
62 | possibles values: any number above 0; default is 10; be mindful that large values may slow down the app significantly
63 |
64 | - combinePanels: (on large screens) combine the panels into one sidebar
65 | possible values: true, false (default)
66 |
67 | - defaultPanel: if "combinePanels" is active (true), which panel is displayed upon loading the app
68 | possible values: "actor", "filters", "tags", "tools"
69 | note: if "combinePanels" is active and if this option is not set, no panel will be initially displayed
70 |
71 | - simplifyPostsDisplay: hide some technical or redundant elements in posts display
72 | possible values: true, false (default)
73 |
74 | The folloging options are specific to server mode and not present in the tools panel:
75 |
76 | - showActorJson: whether or not to display the full JSON data in the actor panel
77 | possible values: true, false (default)
78 |
79 | - showPostsJson: whether or not to display the full JSON data related to each post
80 | possible values: true, false (default)
81 | note: JSON data for posts is always hidden in "simplify post display" mode
82 |
83 | Example:
84 |
85 | const customPrefs = {
86 | pageSize: 5,
87 | combinePanels: true,
88 | defaultPanel: "actor",
89 | simplifyPostsDisplay: true,
90 | };
91 |
92 | */
93 |
--------------------------------------------------------------------------------
/dist/js/i18n/_langs.js:
--------------------------------------------------------------------------------
1 | const appLangs={en:"English",fr:"Français",es:"Español"};let appStrings={};
--------------------------------------------------------------------------------
/dist/js/i18n/en.js:
--------------------------------------------------------------------------------
1 | appStrings.en={welcome:{title:"Welcome to {appName}",p1:"MARL allows you to explore the content of your Mastodon archive file in a\n user-friendly interface. Everything takes place in the browser: your archive stays\n strictly on your computer; none of its data is sent to any server.",p2:'You can request your Mastodon archive by logging into your account on the web,\n then visiting "Preferences > Import and export > Request your archive".
\n Please note: only ZIP files are supported (not GZ).',p3:"Start by opening your archive file with MARL.
\n You can drag and drop it anywhere on this page, or\n {labelStart}click here to select it{labelEnd}."},misc:{loading:"Loading",criticalFailure:"Critical failure",closePanelBtn:"Close panel"},menu:{profile:"Profile",filters:"Filters",tags:"Tags",tools:"Tools",filtersActive:"some filters are active"},lightbox:{next:"Next image",prev:"Previous image",close:"Close image"},actor:{accountInfo:"Account info",accounts:"Accounts",noAvatarImage:"No avatar image",noHeaderImage:"No header image",headerImage:"Header",memberSince:"Member since",countPosts:"posts",countInArchive:"in archive",countDiffWhy:"Why are those two numbers different?",countDiffExplanation:"Posts that are not directly hosted on your instance are kept\n in a cache by your instance for a given time, after what they are deleted from that\n cache. Posts that are not in your instance cache any more are not included in your\n archive. This affects boosts, likes, and bookmarks.",rawData:"Raw data {fileName}",likes:"Favorites",likesEmpty:"no favorites",bookmarks:"Bookmarks",bookmarksEmpty:"no bookmarks"},filters:{panelTitle:"Filter posts",panelNotice:"The list of posts will be automatically updated based on the active\n filters below.",fullText:"Full text",hashtagText:"Hashtags",mentionText:"Mentions",externalLink:"External links",summary:"Summary (CW)",isDuplicate:"Non-exact duplicates",mustContain:"Must contain",hasHashtags:"Hashtag(s)",hasMentions:"Mention(s)",hasPoll:"Poll",hasExternalLink:"External link(s)",hasSummary:"Summary (CW)",activities:"Activities",hasLikes:"likes or more",hasShares:"shares or more",dateTime:"Date & time",afterDate:"or later",beforeDate:"or earlier",afterTime:"or later",beforeTime:"or earlier",type:"Type",typeOriginal:"Original posts (incl. replies)",typeBoost:"Boosts",startingAt:'Starts with "@"',noStartingAt:'Does not start with "@"',isInConversation:"Is part of a thread",isSensitive:"Marked as sensitive",isEdited:"Has been edited",mustHaveAttachement:"Must have attachment",attachmentAny:"Any type",attachmentImage:"Image(s)",attachmentVideo:"Video(s)",attachmentSound:"Sound(s)",attachmentNoAltText:"Without alt text",attachmentWithAltText:"With alt text",visibility:"Visibility",visibilityPublic:"Public",visibilityUnlisted:"Quiet public (Unlisted)",visibilityFollowers:"Followers only",visibilityMentioned:"Mentioned people only",language:"Language",author:"Author",showResults:"Show results",resetFilters:"Reset filters"},header:{countLabel:"posts",oldestFirst:"oldest first",latestFirst:"latest first"},paging:{first:"First",prev:"Prev",next:"Next",last:"Last",page:"Page"},posts:{panelTitle:"Posts",noResults:"No results for the specified filters",noPostsError:"No posts found in archive"},post:{showConversation:"Show thread",by:"by",lastUpdated:"Last updated",linkToPost:"Link",pollType:"Poll type:",pollTypeOne:"One choice only",pollTypeAny:"Multiple choices",pollVotersCount:"Total voters:",pollVotersCountVotes:"({nb} votes)",pollVotersCountAlt:"{nb} voters",pollEndTime:"End time:",pollStatus:"Status:",pollStatusClosed:"Closed",pollStatusRunning:"Running",attachmentNoAlt:"No description provided",attachmentInArchive:"In archive:",attachmentNotFound:"⚠️ Content not found in archive",like1:"1 like",like2:"{nb} likes",share1:"1 share",share2:"{nb} shares",people:"People",hashtags:"Hashtags",extLinks:"External links",rawData:"Raw data"},conversation:{panelTitle:"Thread",panelTitleNbPosts:"({nb} posts in archive)",closePanel:"Close thread",helpSummary:"Why is this thread broken or incomplete?",helpContent:"MARL can only show you posts that are present in your archive. Posts written by other people are typically not included in an archive. If posts are missing in a thread, it's impossible for MARL to reconnect the broken parts. In other words, what you will mostly see here are posts where you reply to yourself (aka threads 🧵)."},tags:{panelTitle:"Tags",hashtags:"Hashtags",mentions:"Mentions",boosts:"Boosted users",hashtagsFilter:"Filter hashtags",mentionsFilter:"Filter mentions",boostsFilter:"Filter boosted users",resetFilter:"reset filter"},tools:{panelTitle:"Tools",appSettings:"App settings",selectLanguage:"Select language",useDarkTheme:"Use dark theme",useLightTheme:"Use light theme",panelsSettings:"Panels",combinePanels:"Combine panels",combinePanelsDesc:"(Wide screens only) Combine all panels into a single sidebar with vertical tabs",defaultPanel:"Default panel",defaultPanelDesc:"Which panel is shown by default when opening the app (if the panels are combined)",lastPanel:"Last panel used",postsSettings:"Posts",postsPerPage:"Number of posts per page:",chronologicalOrder:"Chronological order",chronologicalOrderDesc:"Uncheck to sort posts in reverse chronological order (most recent first)",simplifyPostsDisplay:"Simplify posts display",simplifyPostsDisplayDesc:"Hide some technical or redundant elements",loadedFiles:"Loaded files",loadedRemotes:"Loaded archives",addAnother:"Add another archive",addAnotherTip:"Tip: You can open multiple archives at once.
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)"}};
--------------------------------------------------------------------------------
/dist/js/i18n/es.js:
--------------------------------------------------------------------------------
1 | appStrings.es={welcome:{title:"Bienvenido a {appName}",p1:"MARL le permite explorar el contenido de su archivo Mastodon en un\n \t\tinterfaz fácil de usar. Todo tiene lugar en el navegador: su archivo permanece\n \t\testrictamente en su ordenador; ninguno de sus datos se envía a ningún servidor.",p2:"Puede solicitar su archivo de Mastodon accediendo a su cuenta en la web,\n \t\ty visitando «Preferencias > Importar y exportar > Solicitar su archivo»..
\n \t\tNota: sólo se admiten archivos ZIP (no GZ).",p3:"Empieza abriendo tu archivo con MARL.
\n \t\tPuede arrastrarlo y soltarlo en cualquier lugar de esta página, o\n \t\t{labelStart}haga clic aquí para seleccionarlo{labelEnd}."},misc:{loading:"Cargando",criticalFailure:"fallo crítico",closePanelBtn:"Cerrar panel"},menu:{profile:"Perfil",filters:"Filtros",tags:"Etiquetas",tools:"Herramientas",filtersActive:"algunos filtros están activos"},lightbox:{next:"Imagen siguiente",prev:"imagen previa",close:"Cerrar imagen"},actor:{accountInfo:"Info de cuenta",accounts:"Cuentas",noAvatarImage:"Sin avatar",noHeaderImage:"Sin imagen de cabecera",headerImage:"Cabecera",memberSince:"Miembro desde",countPosts:"publicaciones",countInArchive:"en el archivo",countDiffWhy:"¿Por qué son estos dos números diferentes?",countDiffExplanation:"Los mensajes que no se alojan directamente en su instancia se mantienen\n en una caché durante un tiempo determinado, después de lo cual se eliminan.\n Los mensajes que ya no están en la caché de tu instancia no se incluyen en tu archivo.\n Esto afecta a los impulsos, favoritos y marcadores.",rawData:"Datos en bruto {fileName}",likes:"Favoritos",likesEmpty:"sin favoritos",bookmarks:"Marcadores",bookmarksEmpty:"sin marcadores"},filters:{panelTitle:"Filtrar publicaciones",panelNotice:"La lista de entradas se actualizará automáticamente en función\n \t\tde los siguientes filtros activos.",fullText:"Texto completo",hashtagText:"Etiquetas",mentionText:"Menciones",externalLink:"Enlaces externos",summary:"Aviso de contenido",isDuplicate:"Duplicado no-exacto",mustContain:"debe contener",hasHashtags:"Etiquetas(s)",hasMentions:"Mencion(es)",hasPoll:"Encuesta",hasExternalLink:"Enlace(s) externo(s)",hasSummary:"Aviso de contenido",activities:"Actividades",hasLikes:"favoritos o más",hasShares:"impulsos o más",dateTime:"Fecha & hora",afterDate:"o después",beforeDate:"o antes",afterTime:"o después",beforeTime:"o antes",type:"Tipo",typeOriginal:"Publicación original (incl. respuestas)",typeBoost:"Impulsos",startingAt:'Empieza por "@"',noStartingAt:'No empieza por "@"',isInConversation:"Es parte de un hilo",isSensitive:"Marcado como sensible",isEdited:"Editado",mustHaveAttachement:"Debe tener adjunto",attachmentAny:"Cualquier tipo",attachmentImage:"Imagen(es)",attachmentVideo:"Vídeo(s)",attachmentSound:"Sonido(s)",attachmentNoAltText:"Sin texto alt",attachmentWithAltText:"Con texto alt",visibility:"Visibilidad",visibilityPublic:"Público",visibilityUnlisted:"Silencioso (no listado)",visibilityFollowers:"Sólo seguidores",visibilityMentioned:"Sólo cuentas mencionadas",language:"Idioma",author:"Autor",showResults:"Mostrar resultados",resetFilters:"Reiniciar filtros"},header:{countLabel:"publicaciones",oldestFirst:"desde más antiguo",latestFirst:"desde más reciente"},paging:{first:"Inicio",prev:"Anterior",next:"Siguiente",last:"Fin",page:"Página"},posts:{panelTitle:"Publicaciones",noResults:"Sin resultados para los filtros indicados",noPostsError:"No se encontraron publicaciones en el archivo"},post:{showConversation:"Mostrar hilo",by:"por",lastUpdated:"Última modificación",linkToPost:"Enlace",pollType:"Tipo de encuesta:",pollTypeOne:"Opción única",pollTypeAny:"Opciones múltiples",pollVotersCount:"Votantes totales:",pollVotersCountVotes:"({nb} votos)",pollVotersCountAlt:"{nb} votantes",pollEndTime:"Termina:",pollStatus:"Status:",pollStatusClosed:"Cerrada",pollStatusRunning:"En curso",attachmentNoAlt:"Sin descripción",attachmentInArchive:"En el archivo:",attachmentNotFound:"⚠️ Contenido no encontrado en el archivo",like1:"1 favorito",like2:"{nb} favoritos",share1:"1 impulso",share2:"{nb} impulsos",people:"Gente",hashtags:"Etiquetas",extLinks:"Enlaces externos",rawData:"Datos en bruto"},conversation:{panelTitle:"Hilo",panelTitleNbPosts:"({nb} publicaciones en archivo)",closePanel:"Cerrar hilo",helpSummary:"¿Porque está roto o incompleto este hilo?",helpContent:"MARL sólo puede mostrarte los mensajes que están presentes en tu archivo. Los mensajes escritos por otras personas normalmente no se incluyen en un archivo. Si faltan mensajes en un hilo, es imposible para MARL reconectar las partes rotas. En otras palabras, lo que verás aquí son mensajes en los que te respondes a ti mismo (es decir, hilos 🧵)."},tags:{panelTitle:"Etiquetas",hashtags:"Etiquetas",mentions:"Menciones",boosts:"Usuarios impulsados",hashtagsFilter:"Filtrar etiquetas",mentionsFilter:"Filtrar menciones",boostsFilter:"Filtrar usuarios impulsados",resetFilter:"reiniciar filtro"},tools:{panelTitle:"Herramientas",appSettings:"Ajustes de aplicación",selectLanguage:"Seleccionar idioma",useDarkTheme:"Usar tema oscuro",useLightTheme:"Usar tema claro",panelsSettings:"Paneles",combinePanels:"Combinar paneles",combinePanelsDesc:"(Sólo pantalla anchas) Combinar todos los paneles en una una barra lateral única con pestañas verticales",defaultPanel:"Panel por defecto",defaultPanelDesc:"Qué panel se muestra por defecto al abrir la aplicación (si hay paneles combinados)",lastPanel:"Último panel usado",postsSettings:"Publicaciones",postsPerPage:"Número de publicaciones por página:",chronologicalOrder:"Orden cronológico",chronologicalOrderDesc:"Desmarca para ordenar las publicaciones en orden cronológico inverso (más recientes primero)",simplifyPostsDisplay:"Simplificar visualización de publicaciones",simplifyPostsDisplayDesc:"Ocultar algunos elementos técnicos o redundantes",loadedFiles:"Archivos cargados",loadedRemotes:"Archivos cargados",addAnother:"Añadir otro archivo",addAnotherTip:"Consejo: Puede abrir varios archivos al mismo tiempo.
También puede arrastrar y soltar sus ficheros comprimidos en cualquier lugar de esta ventana.",startOver:"Reiniciar",startOverConfirm:"¿Descartar los datos actuales y cargar un nuevo archivo?",appLog:"Registro de la aplicación",projectPage:"Página del proyecto (github)"}};
--------------------------------------------------------------------------------
/dist/js/i18n/fr.js:
--------------------------------------------------------------------------------
1 | appStrings.fr={welcome:{title:"Bienvenue dans {appName}",p1:"MARL vous permet d'explorer le contenu de votre archive Mastodon dans une\n interface simple d'utilisation. Tout se passe dans votre navigateur : votre archive\n reste sur votre appareil; aucune donnée n'est envoyée à aucun serveur.",p2:'Vous pouvez demander votre archive Mastodon en vous identifiant avec votre compte\n sur le web, puis en visitant "Préférences >Import et export >Demandez vos archives".
\n Important : seuls les fichiers ZIP sont acceptés (pas les fichiers GZ).',p3:"Commencez par ouvrir votre archive avec MARL.
\n Vous pouvez la glisser-déposer n'importe où sur cette page, ou\n {labelStart}cliquer ici pour la sélectionner{labelEnd}."},misc:{loading:"Chargement",criticalFailure:"Erreur fatale",closePanelBtn:"Fermer le panneau"},menu:{profile:"Profil",filters:"Filtres",tags:"Tags",tools:"Outils",filtersActive:"certains filtres sont actifs"},lightbox:{next:"Image suivante",prev:"Image précédente",close:"Fermer l'image"},actor:{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:"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\n sont gardés en cache par celle-ci pour une durée limitée, après quoi ils sont supprimés\n de ce cache. Les posts qui ne sont plus présents dans le cache de votre instance ne sont\n 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:"Filtrer les posts",panelNotice:"La liste des posts sera automatiquement mise à jour en fonction des filtres\n activés ci-dessous.",fullText:"Partout",hashtagText:"Hashtags",mentionText:"Mentions",externalLink:"Liens externes",summary:"Avertissement de contenu",isDuplicate:"Doublons imparfaits",mustContain:"Doit contenir",hasHashtags:"Hashtag(s)",hasMentions:"Mention(s)",hasPoll:"Sondage",hasExternalLink:"Lien(s) externe(s)",hasSummary:"Avertissement de contenu",activities:"Activités",hasLikes:"favoris ou plus",hasShares:"partages ou plus",dateTime:"Date & heure",afterDate:"ou plus tard",beforeDate:"ou plus tôt",afterTime:"ou plus tard",beforeTime:"ou plus tôt",type:"Type",typeOriginal:"Posts originaux (y.c. réponses)",typeBoost:"Partages",startingAt:'Commence par "@"',noStartingAt:'Ne commence pas par "@"',isInConversation:"Fait partie d'un fil",isSensitive:"Marqué comme sensible",isEdited:"A été modifié",mustHaveAttachement:"Doit avoir un fichier joint",attachmentAny:"N'importe quel type",attachmentImage:"Image(s)",attachmentVideo:"Vidéo(s)",attachmentSound:"Son(s)",attachmentNoAltText:"Sans description alternative",attachmentWithAltText:"Avec description alternative",visibility:"Confidentialité",visibilityPublic:"Public",visibilityUnlisted:"Public discret",visibilityFollowers:"Abonnés",visibilityMentioned:"Personnes spécifiques",language:"Langue",author:"Auteur",showResults:"Afficher les résultats",resetFilters:"Réinitialiser les filtres"},header:{countLabel:"posts",oldestFirst:"les plus anciens d'abord",latestFirst:"les plus récents d'abord"},paging:{first:"Première",prev:"Précédente",next:"Suivante",last:"Dernière",page:"Page"},posts:{panelTitle:"Posts",noResults:"Pas de résultats pour les filtres spécifiés",noPostsError:"Aucun post trouvé dans l'archive"},post:{showConversation:"Voir le fil",by:"par",lastUpdated:"Dernière modification",linkToPost:"Lien",pollType:"Type de sondage :",pollTypeOne:"Choix unique",pollTypeAny:"Choix multiples",pollVotersCount:"Nombre de votants :",pollVotersCountVotes:"({nb} votes)",pollVotersCountAlt:"{nb} votants",pollEndTime:"Date de fin :",pollStatus:"État :",pollStatusClosed:"Terminé",pollStatusRunning:"En cours",attachmentNoAlt:"Aucune description fournie",attachmentInArchive:"Dans l'archive :",attachmentNotFound:"⚠️ Média introuvable à l'emplacement indiqué",like1:"1 favori",like2:"{nb} favoris",share1:"1 partage",share2:"{nb} partages",people:"Personnes",hashtags:"Hashtags",extLinks:"Liens externes",rawData:"Données brutes"},conversation:{panelTitle:"Fil",panelTitleNbPosts:"({nb} posts dans l'archive)",closePanel:"Fermer le fil",helpSummary:"Pourquoi ce fil est-il cassé ou incomplet ?",helpContent:"MARL peut seulement afficher les posts qui sont présents dans votre archive. De manière générale, les posts rédigés par d'autres personnes ne font pas partie du contenu d'une archive. Si un fil est cassé du fait de l'absence d'un post, il est impossible pour MARL de reconnecter les fragments. En d'autres termes, ce que vous verrez ici, ce sont principalement des réponses à vous-même (autrement dit, des fils ou threads 🧵)."},tags:{panelTitle:"Tags",hashtags:"Hashtags",mentions:"Mentions",boosts:"Utilisateurs partagés",hashtagsFilter:"Filtrer les hashtags",mentionsFilter:"Filtrer les mentions",boostsFilter:"Filter utilisateurs partagés",resetFilter:"réinitialiser le filtre"},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",panelsSettings:"Panneaux",combinePanels:"Combiner les panneaux",combinePanelsDesc:"(Sur les écrans larges) Combiner les différents panneaux en une seule barre latérale avec onglets verticaux",defaultPanel:"Panneau par défaut",defaultPanelDesc:"Quel panneau est affiché par défaut au chargement de l'app (si les panneaux sont combinés)",lastPanel:"Dernier panneau utilisé",postsSettings:"Posts",postsPerPage:"Nombre de posts page page :",chronologicalOrder:"Ordre chronologique",chronologicalOrderDesc:"Décocher pour trier les posts dans l'ordre chronologique inverse (les plus récents en premier)",simplifyPostsDisplay:"Simplifier l'affichage des posts",simplifyPostsDisplayDesc:"Cacher certains éléments techniques ou redondants",loadedFiles:"Fichiers chargés",loadedRemotes:"Archives chargées",addAnother:"Ajouter une autre archive",addAnotherTip:"Astuce: Vous pouvez ouvrir plusieurs archives en même temps.
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)"}};
--------------------------------------------------------------------------------
/dist/js/libs/alpinejs-i18n.min.js:
--------------------------------------------------------------------------------
1 | (()=>{var c=new Event("alpine-i18n:locale-change"),o=new Event("alpine-i18n:ready"),r={version:"2.5.2",set locale(e){this.checkLocale(e),this.currentLocale=e,document.dispatchEvent(c)},get locale(){return this.currentLocale},currentLocale:"",messages:{},fallbackLocale:"",options:{},create(e,i,n){this.messages=i,this.checkLocale(e),this.locale=e,this.options=n},checkLocale(e){if(!Object.keys(this.messages).includes(e))throw new Error(`Alpine I18n: The locale ${e} does not exist.`)},t(e,i){let n="";try{n=e.split(".").reduce((t,l)=>t[l],this.messages[this.locale])}catch{}if(!n&&this.fallbackLocale.length)n=e.split(".").reduce((t,l)=>t[l],this.messages[this.fallbackLocale]);else if(!n)return this.options?.debug?`???${e}`:e;for(let t in i)if(n&&n.replaceAll&&Object.prototype.hasOwnProperty.call(i,t)){let l=i[t],a=new RegExp("{s*("+t+")s*}","g");n=n.replaceAll(a,l)}return this.options?.debug?`[${n}]`:n}};function s(e){window.AlpineI18n=e.reactive(r),document.dispatchEvent(o),e.magic("locale",i=>n=>{if(!n)return window.AlpineI18n.locale;window.AlpineI18n.locale=n}),e.magic("t",i=>(n,t)=>window.AlpineI18n.t(n,t))}document.addEventListener("alpine:initializing",()=>{s(window.Alpine)});})();
2 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/screenshots/screenshot-dark-theme.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/s427/MARL/f173ee599493b68b2d25cb47e9c09cea677dd974/screenshots/screenshot-dark-theme.png
--------------------------------------------------------------------------------
/screenshots/screenshot-multiple-archives.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/s427/MARL/f173ee599493b68b2d25cb47e9c09cea677dd974/screenshots/screenshot-multiple-archives.png
--------------------------------------------------------------------------------
/screenshots/screenshot-simpler-layout.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/s427/MARL/f173ee599493b68b2d25cb47e9c09cea677dd974/screenshots/screenshot-simpler-layout.png
--------------------------------------------------------------------------------
/server-mode.md:
--------------------------------------------------------------------------------
1 | # "Server mode" : Using MARL as a publishing tool
2 |
3 | Starting from version 2.3.2, MARL can be used in what is called "server mode", as opposed to the default "local mode".
4 |
5 | Those two modes are mutually exclusive.
6 |
7 | ## Local vs. Server
8 |
9 | ### TL;DR
10 |
11 | __Local mode:__
12 |
13 | - The default mode
14 | - The easiest to use
15 | - Completely private
16 | - May not work with very large archives
17 | (possible workaround: remove attachments from archive)
18 |
19 | __Server mode:__
20 |
21 | - A bit more technical (web server required)
22 | - Requires a bit of setup
23 | - Allows to make your archive public
24 | ⚠️ privacy considerations: remove private posts from your archive first; a tool is provided, see below
25 | - Can also be used in a private way (local web server)
26 | - Will handle very large archives better than local mode
27 |
28 | ### Lengthy explanation
29 |
30 | __Local mode__ is the default. It simply allows the user to explore the content of their archive in the privacy of their own device. In this mode, MARL will start by showing a welcome screen and invite the user to select their archive file, located in their _local file system_. Once they do so, MARL unpacks and displays the content of the selected file. Everything takes place strictly in the browser, and no data is sent anywhere.
31 |
32 | In __server mode__, MARL will automatically load one or several _specific_ archives located on a web server, as specified in its configuration (see below). Only the specified archive(s) will be loaded: the user will NOT be able to load their own archive via drag and drop or a file selection field. This allows you to use MARL as a publication tool, making your archive viewable by anybody on the web. Of course it can also be used on a local web server.
33 |
34 | In server mode, MARL does not load the entire ZIP file in the browser: it starts by fetching only the four JSON files at the archive root. The media attachments (images, videos or sounds) are then fetched individually from the server, and only when MARL actually needs to display them to the user.
35 |
36 | One limitation of __local mode__ is that if your archive file is very large (e.g. several gigabytes), it may fail to load because it exceeds the browser cache capacity. One imperfect workaround is to delete the `media_attachments` folder from your archive (by repackaging it), in which case you will be able to explore your posts without any attachments. Another solution is to use server mode, which can be also done on a _local_ web server. In this case your data remains private (you don't need to put it on a public server) and you should be able to explore your archive with all the media attachments.
37 |
38 | ## Privacy considerations (⚠️ _please read carefully_ ⚠️)
39 |
40 | In server mode, MARL still operates in the same, browser-centric logic as in local mode: _all data related to your profile, posts, bookmarks and likes is first loaded to the browser_, which is then tasked with performing all the operations requested by the user (filtering posts, paging, search etc).
41 |
42 | _This means that anybody accessing MARL in server mode basically gains full access to your archive._
43 | If you are not comfortable with that, you should not use it in this way (or at least not make it public).
44 |
45 | ### What about private posts?
46 |
47 | By default, `outbox.json` also contains your __private posts__. As this is not something that most users would want when making their archive public, the MARL project includes a script that will take your outbox file and generate a new file, removing all private posts in it.
48 |
49 | ⚠️ __Please understand that this operation is not automatic.__ It is your responsibility to execute the script in order to remove your private posts from your archive prior to publishing it. In order to avoid any faux-pas, MARL operating in server mode will not fetch the `outbox.json` under its original name; instead it will look for a file named `outbox-public.json`. This is to (hopefully) avoid people publishing their archive without realizing that it can make their private posts public.
50 |
51 | - If you use the `outbox-cleanup` script, it will automatically generate the file with the correct name. Do not forget to _delete the original `outbox.json` file_ from your archive folder when you put it online (MARL will not attempt to access it, but other people may try).
52 | - If you want all your posts (private included) to be published, simply rename your outbox file to `outbox-public.json`. No need to use the script.
53 |
54 | ### What about bookmarks and likes?
55 |
56 | If you do not want to publish your bookmarks and/or liked posts, simply remove the corresponding files from your archive (`bookmarks.json` or `likes.json`). MARL will still work without those files.
57 |
58 | ## Enable server mode
59 |
60 | To use MARL in server mode, you need to:
61 |
62 | 1. __Unpack your archive__
63 | This is important: in server mode, MARL will not do the unpacking of the ZIP file; instead, it will fetch files from your archive individually. So they have to be unpacked beforehand.
64 | 2. __Remove your private posts from `outbox.json`__ (script available)
65 | You can find this script in the `tools/outbox-cleanup` folder of the MARL project. Please refer to the [`readme.md`](./tools/outbox-cleanup/readme.md) file in this folder for more information.
66 |
67 | 3. __Upload your unpacked archive on a web server__
68 | 4. __Configure MARL to tell it where it can find your unpacked archive__
69 | The configuration takes place in the `js/config.js` file of the `dist` folder (or whatever folder MARL is located in). It contains more information about how to use it. If this file is not present, please copy and rename it from `js/config.default.js`.
70 | 5. __Upload MARL on a web server__
71 |
72 | That's it! When it loads the configuration (step 4), MARL will automatically switch to server mode.
73 |
74 | ## Change the default options
75 |
76 | You can also change de default values for all the options found in the "Tools" panel in the app (e.g. number of posts per page, combine panels, simplify post display etc). This also takes place in the `js/config.js` file. Please refer to this file for more information.
77 |
78 | Note that this only changes the _default_ values for those options. Users will still be able to use the Tools panel to set different options for themselves if they wish.
79 |
--------------------------------------------------------------------------------
/tools/outbox-cleanup/commands/cleanup.js:
--------------------------------------------------------------------------------
1 | import { readFileSync, writeFileSync, existsSync } from "fs";
2 | import readline from "readline";
3 | import chalk from "chalk";
4 |
5 | const verbose = false;
6 |
7 | function cleanup(options) {
8 | let json = "";
9 | let level = 2;
10 | let levelDesc = "";
11 | let noBoosts = false;
12 |
13 | if (options) {
14 | if (options.privacy) {
15 | switch (+options.privacy) {
16 | case 1:
17 | case 2:
18 | case 3:
19 | case 4:
20 | level = +options.privacy;
21 | break;
22 | default:
23 | console.log(
24 | chalk.red(`Invalid option provided (${options.privacy}); falling back to default value (${level}).`)
25 | );
26 | break;
27 | }
28 | }
29 | if (options.noboosts) {
30 | noBoosts = true;
31 | }
32 | }
33 |
34 | switch (level) {
35 | case 1:
36 | levelDesc = "only keep public posts";
37 | break;
38 | case 2:
39 | levelDesc = "only keep public and unlisted posts";
40 | break;
41 | case 3:
42 | levelDesc = "only keep public, unlisted, and followers-only posts";
43 | break;
44 | case 4:
45 | levelDesc = "keep all posts";
46 | break;
47 | }
48 |
49 | console.log(chalk.cyan("MARL cleanup"));
50 | console.log(chalk.cyan(` - privacy level: ${level} (${levelDesc})`));
51 | if (noBoosts) {
52 | console.log(chalk.cyan(` - exclude boosts`));
53 | }
54 |
55 | const opt = {
56 | level: level,
57 | noBoosts: noBoosts,
58 | };
59 |
60 | try {
61 | const data = readFileSync("./outbox.json");
62 | json = JSON.parse(data);
63 | console.log(chalk.cyan("'outbox.json' successfully read."));
64 | } catch (error) {
65 | console.log(chalk.red("Error loading 'outbox.json'. Aborting."));
66 | if (verbose) {
67 | console.error(error);
68 | }
69 | return;
70 | }
71 |
72 | if (existsSync("./outbox-public.json")) {
73 | const rl = readline.createInterface({
74 | input: process.stdin,
75 | output: process.stdout,
76 | });
77 |
78 | console.log(chalk.yellow("Notice: 'outbox-public.json' already exists!"));
79 | rl.question(`Overwrite? (Y|n) `, (answer) => {
80 | rl.close();
81 |
82 | if (answer === "" || answer.toLocaleLowerCase() === "y") {
83 | console.log(chalk.yellow("Overwriting..."));
84 | writeFile(json, opt);
85 | } else {
86 | console.log(chalk.yellow("Aborting."));
87 | return;
88 | }
89 | });
90 | } else {
91 | writeFile(json, opt);
92 | }
93 | }
94 |
95 | function filterPosts(json, opt) {
96 | const level = opt.level;
97 | const noBoosts = opt.noBoosts;
98 |
99 | const filteredItems = json.orderedItems.filter((item) => {
100 | if (noBoosts && item.type === "Announce") {
101 | // this is a boost
102 | return false;
103 | }
104 |
105 | // conditions borrowed from MARL (utils.js > tootVisibility())
106 | if (item.to.includes("https://www.w3.org/ns/activitystreams#Public")) {
107 | // post is public
108 | return true;
109 | }
110 | if (
111 | item.to.some((x) => x.indexOf("/followers") > -1) &&
112 | !item.to.includes("https://www.w3.org/ns/activitystreams#Public") &&
113 | item.cc.includes("https://www.w3.org/ns/activitystreams#Public")
114 | ) {
115 | // post is unlisted
116 | return level >= 2;
117 | }
118 | if (
119 | item.to.some((x) => x.indexOf("/followers") > -1) &&
120 | !item.to.includes("https://www.w3.org/ns/activitystreams#Public") &&
121 | !item.cc.includes("https://www.w3.org/ns/activitystreams#Public")
122 | ) {
123 | // post is followers only
124 | return level >= 3;
125 | }
126 | if (
127 | !item.to.some((x) => x.indexOf("/followers") > -1) &&
128 | !item.to.includes("https://www.w3.org/ns/activitystreams#Public") &&
129 | !item.cc.includes("https://www.w3.org/ns/activitystreams#Public")
130 | ) {
131 | // post is private (mentioned people only)
132 | return level >= 4;
133 | }
134 | });
135 | json.orderedItems = filteredItems;
136 |
137 | return json;
138 | }
139 |
140 | function writeFile(json, opt) {
141 | if (json && json.orderedItems) {
142 | const path = "./outbox-public.json";
143 |
144 | json = filterPosts(json, opt);
145 |
146 | try {
147 | writeFileSync(path, JSON.stringify(json), "utf8");
148 | console.log(chalk.cyan("'outbox-public.json' successfully written to disk."));
149 | } catch (error) {
150 | console.log(chalk.red("Error writing 'outbox-public.json' to disk."));
151 | if (verbose) {
152 | console.error(error);
153 | }
154 | }
155 | } else {
156 | console.log(chalk.red("Invalid JSON structure. Aborting"));
157 | }
158 | }
159 |
160 | export default cleanup;
161 |
--------------------------------------------------------------------------------
/tools/outbox-cleanup/index.js:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env node
2 |
3 | import { program } from "commander";
4 | import cleanup from "./commands/cleanup.js";
5 |
6 | program
7 | .command("cleanup")
8 | .description(
9 | `Reads 'outbox.json' and writes its content to 'outbox-public.json' after removing posts based on their visibility level.`
10 | )
11 | .option(
12 | "-p, --privacy ",
13 | `
14 | The privacy level of the output, ie which type of post to include. Default is 2.
15 | Possible values:
16 | 1: only keep public posts
17 | 2: only keep public and unlisted posts
18 | 3: only keep public, unlisted, and followers-only posts
19 | 4: keep all posts
20 | `
21 | )
22 | .option("-b, --noboosts", "Remove all boosts from archive.")
23 | .action(cleanup);
24 |
25 | program.parse();
26 |
--------------------------------------------------------------------------------
/tools/outbox-cleanup/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "outbox-cleanup",
3 | "version": "1.0.0",
4 | "lockfileVersion": 3,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "name": "outbox-cleanup",
9 | "version": "1.0.0",
10 | "license": "ISC",
11 | "dependencies": {
12 | "chalk": "^5.4.1",
13 | "commander": "^13.1.0"
14 | },
15 | "bin": {
16 | "marl": "index.js"
17 | }
18 | },
19 | "node_modules/chalk": {
20 | "version": "5.4.1",
21 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz",
22 | "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==",
23 | "engines": {
24 | "node": "^12.17.0 || ^14.13 || >=16.0.0"
25 | },
26 | "funding": {
27 | "url": "https://github.com/chalk/chalk?sponsor=1"
28 | }
29 | },
30 | "node_modules/commander": {
31 | "version": "13.1.0",
32 | "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz",
33 | "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==",
34 | "engines": {
35 | "node": ">=18"
36 | }
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/tools/outbox-cleanup/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "outbox-cleanup",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "type": "module",
6 | "bin": {
7 | "marl": "./index.js"
8 | },
9 | "author": "",
10 | "license": "ISC",
11 | "description": "",
12 | "dependencies": {
13 | "chalk": "^5.4.1",
14 | "commander": "^13.1.0"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/tools/outbox-cleanup/readme.md:
--------------------------------------------------------------------------------
1 | # MARL tool - outbox cleanup
2 |
3 | A simple script that reads the content of the `outbox.json` in the current directory, and writes its content in a `outbox-public.json` file, after removing posts based on their visibility level.
4 |
5 | Requires [Node.js](https://nodejs.org/en).
6 |
7 | ## Installation
8 |
9 | From the tool directory (where `packages.json` is located):
10 |
11 | First install the dependencies:
12 |
13 | npm i
14 |
15 | Then install the tool globally:
16 |
17 | npm i -g
18 |
19 | ## Usage
20 |
21 | The following commands should be issued from the folder where your `outbox.json` file is located.
22 |
23 | Basic command:
24 |
25 | marl cleanup
26 |
27 | The script will look for a file named `outbox.json` in the current folder and output a file named `outbox-public.json` in the same folder. If such a file already exists, the user is asked to confirm whether it should be overwritten or not.
28 |
29 | By default:
30 | - The script will remove posts that are _private_ (mentioned users only) or for _followers only_; in other words, only the public and unlisted posts are kept.
31 | - Boosts are preserved.
32 |
33 | ## Options
34 |
35 | ### Visibility level
36 |
37 | You can specifiy which types of posts get removed from the output based on their visibility level:
38 |
39 | marl cleanup -p [CODE]
40 | marl cleanup --privacy [CODE]
41 |
42 | The following codes are available:
43 |
44 | - `1`: only keep public posts
45 | - `2`: only keep public and unlisted posts
46 | - `3`: only keep public, unlisted, and followers-only posts
47 | - `4`: keep all posts (useful if you only want to remove your boosts; see below)
48 |
49 | The default value is `2`.
50 |
51 | ### Excluding boosts
52 |
53 | You can exclude all boosts from the output:
54 |
55 | marl cleanup -b
56 | marl cleanup --noboosts
57 |
58 | ### Help
59 |
60 | marl cleanup --help
61 |
62 |
--------------------------------------------------------------------------------
/webxdc/.gitignore:
--------------------------------------------------------------------------------
1 | marl.xdc
2 |
--------------------------------------------------------------------------------
/webxdc/Makefile:
--------------------------------------------------------------------------------
1 | build:
2 | rm -f marl.xdc
3 | (cd ../dist && zip -9 --recurse-paths - index.html _astro/ js/) > marl.xdc
4 | zip marl.xdc manifest.toml icon.png
5 |
--------------------------------------------------------------------------------
/webxdc/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/s427/MARL/f173ee599493b68b2d25cb47e9c09cea677dd974/webxdc/icon.png
--------------------------------------------------------------------------------
/webxdc/manifest.toml:
--------------------------------------------------------------------------------
1 | name = "MARL"
2 | source_code_url = "https://github.com/s427/MARL"
3 |
--------------------------------------------------------------------------------
/webxdc/readme.md:
--------------------------------------------------------------------------------
1 | # MARL as a `webxdc` app
2 |
3 | This folder contains the necessary files to build a [`webxdc`](https://webxdc.org) version of `MARL`.
4 |
5 | ## Build
6 |
7 | To build the `webxdc` app type `make` in the folder. This will create a `marl.xdc` file, which is a zip file. It should be attached to a tagged release in order to be picked up by the webxdc app store.
8 |
9 | ## Files
10 |
11 | * `Makefile` for building `marl.xdc`
12 | * `manifest.toml` manifest file with basic metadata about the app
13 | * `icon.png` app icon, bitmap version
14 | * `icon.svg` app icon, vectorized version
15 |
--------------------------------------------------------------------------------
95 | 96 | 99 | 100 | 101 |
102 |