Skip to content

Commit e83d70f

Browse files
authored
Merge pull request #42 from koloml/release/0.3
Release: 0.3
2 parents f9cb73b + 844853f commit e83d70f

25 files changed

+1263
-531
lines changed

manifest.json

+28-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "Furbooru Tagging Assistant",
33
"description": "Experimental extension with a set of tools to make the tagging faster and easier. Made specifically for Furbooru.",
4-
"version": "0.2.1",
4+
"version": "0.3.0",
55
"browser_specific_settings": {
66
"gecko": {
77
@@ -23,7 +23,8 @@
2323
"*://*.furbooru.org/",
2424
"*://*.furbooru.org/images?*",
2525
"*://*.furbooru.org/search?*",
26-
"*://*.furbooru.org/tags/*"
26+
"*://*.furbooru.org/tags/*",
27+
"*://*.furbooru.org/galleries/*"
2728
],
2829
"js": [
2930
"src/content/listing.js"
@@ -39,6 +40,31 @@
3940
"js": [
4041
"src/content/header.js"
4142
]
43+
},
44+
{
45+
"matches": [
46+
"*://*.furbooru.org/images?*",
47+
"*://*.furbooru.org/images/*",
48+
"*://*.furbooru.org/images/*/tag_changes",
49+
"*://*.furbooru.org/images/*/tag_changes?*",
50+
"*://*.furbooru.org/search?*",
51+
"*://*.furbooru.org/tags?*",
52+
"*://*.furbooru.org/tags/*",
53+
"*://*.furbooru.org/profiles/*/tag_changes",
54+
"*://*.furbooru.org/profiles/*/tag_changes?*",
55+
"*://*.furbooru.org/filters/*"
56+
],
57+
"js": [
58+
"src/content/tags.js"
59+
]
60+
},
61+
{
62+
"matches": [
63+
"*://*.furbooru.org/images/*"
64+
],
65+
"js": [
66+
"src/content/tags-editor.js"
67+
]
4268
}
4369
],
4470
"action": {

package-lock.json

+500-430
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "furbooru-tagging-assistant",
3-
"version": "0.2.1",
3+
"version": "0.3.0",
44
"private": true,
55
"scripts": {
66
"build": "npm run build:popup && npm run build:extension",
@@ -17,10 +17,10 @@
1717
"@types/chrome": "^0.0.262",
1818
"cheerio": "^1.0.0-rc.12",
1919
"sass": "^1.71.0",
20-
"svelte": "^4.2.7",
20+
"svelte": "^4.2.19",
2121
"svelte-check": "^3.6.0",
2222
"typescript": "^5.0.0",
23-
"vite": "^5.0.3"
23+
"vite": "^5.4.9"
2424
},
2525
"type": "module",
2626
"dependencies": {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
<script>
2+
import Menu from "$components/ui/menu/Menu.svelte";
3+
import MenuItem from "$components/ui/menu/MenuItem.svelte";
4+
import {storagesCollection} from "$stores/debug.js";
5+
import {goto} from "$app/navigation";
6+
import {findDeepObject} from "$lib/utils.js";
7+
8+
/** @type {string} */
9+
export let storage;
10+
11+
/** @type {string[]} */
12+
export let path;
13+
14+
/** @type {Object|null} */
15+
let targetStorage = null;
16+
/** @type {[string, string][]} */
17+
let breadcrumbs = [];
18+
/** @type {Object<string, any>|null} */
19+
let targetObject = null;
20+
let targetPathString = '';
21+
22+
$: {
23+
/** @type {[string, string][]} */
24+
const builtBreadcrumbs = [];
25+
26+
breadcrumbs = path.reduce((resultCrumbs, entry) => {
27+
let entryPath = entry;
28+
29+
if (resultCrumbs.length) {
30+
entryPath = resultCrumbs[resultCrumbs.length - 1][1] + "/" + entryPath;
31+
}
32+
33+
resultCrumbs.push([entry, entryPath]);
34+
35+
return resultCrumbs;
36+
}, builtBreadcrumbs);
37+
38+
targetPathString = path.join("/");
39+
40+
if (targetPathString.length) {
41+
targetPathString += "/";
42+
}
43+
}
44+
45+
$: {
46+
targetStorage = $storagesCollection[storage];
47+
48+
if (!targetStorage) {
49+
goto("/preferences/debug/storage");
50+
}
51+
}
52+
53+
$: {
54+
targetObject = targetStorage
55+
? findDeepObject(targetStorage, path)
56+
: null;
57+
}
58+
</script>
59+
60+
<Menu>
61+
<MenuItem href="/preferences/debug/storage" icon="arrow-left">Back</MenuItem>
62+
<hr>
63+
</Menu>
64+
<p class="path">
65+
<span>/ <a href="/preferences/debug/storage/{storage}">{storage}</a></span>
66+
{#each breadcrumbs as [name, entryPath]}
67+
<span>/ <a href="/preferences/debug/storage/{storage}/{entryPath}/">{name}</a></span>
68+
{/each}
69+
</p>
70+
{#if targetObject}
71+
<Menu>
72+
<hr>
73+
{#each Object.entries(targetObject) as [key, value]}
74+
{#if targetObject[key] && typeof targetObject[key] === 'object'}
75+
<MenuItem href="/preferences/debug/storage/{storage}/{targetPathString}{key}">
76+
{key}: Object
77+
</MenuItem>
78+
{:else}
79+
<MenuItem>
80+
{key}: {typeof targetObject[key]} = {targetObject[key]}
81+
</MenuItem>
82+
{/if}
83+
{/each}
84+
</Menu>
85+
{/if}
86+
87+
<style lang="scss">
88+
.path {
89+
display: flex;
90+
flex-wrap: wrap;
91+
column-gap: .5em;
92+
}
93+
</style>

src/content/tags-editor.js

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import {TagsForm} from "$lib/components/TagsForm.js";
2+
3+
TagsForm.watchForEditors();

src/content/tags.js

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import {wrapTagDropdown} from "$lib/components/TagDropdownWrapper.js";
2+
3+
for (let tagDropdownElement of document.querySelectorAll('.tag.dropdown')) {
4+
wrapTagDropdown(tagDropdownElement);
5+
}
+211
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
import {BaseComponent} from "$lib/components/base/BaseComponent.js";
2+
3+
export class FullscreenViewer extends BaseComponent {
4+
/** @type {HTMLVideoElement} */
5+
#videoElement;
6+
/** @type {HTMLImageElement} */
7+
#imageElement;
8+
9+
/** @type {number|null} */
10+
#touchId = null;
11+
/** @type {number|null} */
12+
#startX = null;
13+
/** @type {number|null} */
14+
#startY = null;
15+
/** @type {boolean|null} */
16+
#isClosingSwipeStarted = null;
17+
18+
/**
19+
* @protected
20+
*/
21+
build() {
22+
this.container.classList.add('fullscreen-viewer');
23+
24+
this.#videoElement = document.createElement('video');
25+
this.#imageElement = document.createElement('img');
26+
}
27+
28+
/**
29+
* @protected
30+
*/
31+
init() {
32+
document.addEventListener('keydown', this.#onDocumentKeyPressed.bind(this));
33+
this.on('click', this.#close.bind(this));
34+
35+
this.on('touchstart', this.#onTouchStart.bind(this));
36+
this.on('touchmove', this.#onTouchMove.bind(this));
37+
this.on('touchend', this.#onTouchEnd.bind(this));
38+
}
39+
40+
/**
41+
* @param {TouchEvent} event
42+
*/
43+
#onTouchStart(event) {
44+
if (this.#touchId !== null) {
45+
return;
46+
}
47+
48+
const firstTouch = event.touches.item(0);
49+
50+
if (!firstTouch) {
51+
return;
52+
}
53+
54+
this.#touchId = firstTouch.identifier;
55+
this.#startX = firstTouch.clientX;
56+
this.#startY = firstTouch.clientY;
57+
this.container.classList.add(FullscreenViewer.#swipeState);
58+
}
59+
60+
/**
61+
* @param {TouchEvent} event
62+
*/
63+
#onTouchEnd(event) {
64+
if (this.#touchId === null) {
65+
return;
66+
}
67+
68+
const endedTouch = Array.from(event.changedTouches)
69+
.find(touch => touch.identifier === this.#touchId);
70+
71+
if (!endedTouch) {
72+
return;
73+
}
74+
75+
const verticalDistance = Math.abs(endedTouch.clientY - this.#startY);
76+
const requiredClosingDistance = window.innerHeight / 3;
77+
78+
if (this.#isClosingSwipeStarted && verticalDistance > requiredClosingDistance) {
79+
this.#close();
80+
}
81+
82+
this.#touchId = null;
83+
this.#startX = null;
84+
this.#startY = null;
85+
this.#isClosingSwipeStarted = null;
86+
87+
this.container.classList.remove(FullscreenViewer.#swipeState);
88+
89+
requestAnimationFrame(() => {
90+
this.container.style.removeProperty(FullscreenViewer.#offsetProperty);
91+
this.container.style.removeProperty(FullscreenViewer.#opacityProperty);
92+
});
93+
}
94+
95+
/**
96+
* @param {TouchEvent} event
97+
*/
98+
#onTouchMove(event) {
99+
if (this.#touchId === null) {
100+
return;
101+
}
102+
103+
if (this.#isClosingSwipeStarted === false) {
104+
return;
105+
}
106+
107+
for (const changedTouch of event.changedTouches) {
108+
if (changedTouch.identifier !== this.#touchId) {
109+
continue;
110+
}
111+
112+
const verticalDistance = changedTouch.clientY - this.#startY;
113+
114+
if (this.#isClosingSwipeStarted === null) {
115+
const horizontalDistance = changedTouch.clientX - this.#startX;
116+
117+
if (Math.abs(verticalDistance) >= FullscreenViewer.#minRequiredDistance) {
118+
this.#isClosingSwipeStarted = true;
119+
} else if (Math.abs(horizontalDistance) >= FullscreenViewer.#minRequiredDistance) {
120+
this.#isClosingSwipeStarted = false;
121+
break;
122+
} else {
123+
break;
124+
}
125+
}
126+
127+
this.container.style.setProperty(
128+
FullscreenViewer.#offsetProperty,
129+
verticalDistance.toString().concat('px')
130+
);
131+
132+
const maxDistance = window.innerHeight * 2;
133+
let opacity = 1;
134+
135+
if (verticalDistance !== 0) {
136+
opacity -= Math.min(1, Math.abs(verticalDistance) / maxDistance);
137+
}
138+
139+
this.container.style.setProperty(
140+
FullscreenViewer.#opacityProperty,
141+
opacity.toString()
142+
);
143+
144+
break;
145+
}
146+
}
147+
148+
/**
149+
* @param {KeyboardEvent} event
150+
*/
151+
#onDocumentKeyPressed(event) {
152+
if (event.code === 'Escape' || event.code === 'Esc') {
153+
this.#close();
154+
}
155+
}
156+
157+
#close() {
158+
this.container.classList.remove(FullscreenViewer.#shownState);
159+
document.body.style.overflow = null;
160+
161+
requestAnimationFrame(() => {
162+
this.#videoElement.volume = 0;
163+
this.#videoElement.pause();
164+
this.#videoElement.remove();
165+
});
166+
}
167+
168+
/**
169+
* @param {string} url
170+
*/
171+
show(url) {
172+
requestAnimationFrame(() => {
173+
this.container.classList.add(FullscreenViewer.#shownState);
174+
document.body.style.overflow = 'hidden';
175+
});
176+
177+
if (FullscreenViewer.#isVideoUrl(url)) {
178+
this.#imageElement.remove();
179+
180+
this.#videoElement.src = url;
181+
this.#videoElement.volume = 0;
182+
this.#videoElement.autoplay = true;
183+
this.#videoElement.loop = true;
184+
this.#videoElement.controls = true;
185+
186+
this.container.append(this.#videoElement);
187+
188+
return;
189+
}
190+
191+
this.#videoElement.remove();
192+
193+
this.#imageElement.src = url;
194+
195+
this.container.append(this.#imageElement);
196+
}
197+
198+
/**
199+
* @param {string} url
200+
* @return {boolean}
201+
*/
202+
static #isVideoUrl(url) {
203+
return url.endsWith('.mp4') || url.endsWith('.webm');
204+
}
205+
206+
static #offsetProperty = '--offset';
207+
static #opacityProperty = '--opacity';
208+
static #shownState = 'shown';
209+
static #swipeState = 'swiped';
210+
static #minRequiredDistance = 50;
211+
}

0 commit comments

Comments
 (0)