Compare commits

...

40 Commits

Author SHA1 Message Date
Nischay
f545606bf1 Fix HTML link tag 2024-05-12 22:47:12 +05:30
Nischay
0c70fad5c3 Move styles to sass, add details about giphy_mxc_prefix 2024-05-12 22:45:42 +05:30
Nischay
d14271d036 Remove unncessary extension from URL and erroneous script tag 2024-05-12 22:14:27 +05:30
Nischay
ca99c5f00d Clean up code and remove dependency on matrix-widget-api. Use giphy.mau.dev for MSC3860 redirects 2024-05-12 21:53:39 +05:30
Nischay
398fc952b6 Revert "Merge branch 'master' into gif-search-matrix-widget-api"
This reverts commit 91eca68c7b, reversing
changes made to 182cafe13a.
2024-05-12 10:42:20 +05:30
Nischay Hegde
91eca68c7b Merge branch 'master' into gif-search-matrix-widget-api 2024-05-12 10:38:58 +05:30
Nischay
182cafe13a Fix CSS in the tab-container div 2024-05-12 10:13:06 +05:30
Nischay
c4588f19a7 Feat: Conditional loading of the GIF search tab based on API Key 2024-05-11 15:47:58 +05:30
Nischay
2e1b333cbb Remove thumbnail_url 2024-04-29 21:27:46 +05:30
Nischay
64b7b1507f It's ALIIIIIIIVEEEE! 2024-04-29 20:17:07 +05:30
Nischay
0897ce6c20 Buggy code, no idea why toWidget gets no response 2024-04-21 19:44:54 +05:30
Nischay
380a070e71 Improve comments 2024-04-21 11:23:11 +05:30
Nischay
59bf7ef252 Converted gif-search code to use matrix-widget-api 2024-04-21 11:16:03 +05:30
lol
8c4291d266 Add anime sticker pack 2023-07-19 09:48:59 +05:30
lol
cfd0b8e292 Add more stickers 2023-07-01 10:33:39 +05:30
Tulir Asokan
f59406a47a Add missing parameter to getting sticker sets. Fixes #51 2022-11-15 12:58:38 +02:00
Tulir Asokan
99ced8878a Merge remote-tracking branch 'salixor/feat/search-box' 2021-10-03 12:52:55 +03:00
Tulir Asokan
046779d102 Update some metadata 2021-10-03 12:45:37 +03:00
Tulir Asokan
ef844a0ff8 Update dependencies 2021-10-03 12:42:11 +03:00
Tulir Asokan
502d91fc75 Merge remote-tracking branch 'p1gp1g/dev' 2021-10-03 11:50:47 +03:00
Tulir Asokan
591137ccb3 Merge remote-tracking branch 'celogeek/fixes-ios-and-more' 2021-10-03 11:50:27 +03:00
Tulir Asokan
f29c165357 Merge remote-tracking branch 'aWeinzierl/fix-encoding' 2021-10-03 11:49:53 +03:00
S1m
7939793351 Remove parseQuery and use params 2021-09-30 08:52:41 +02:00
S1m
e0d895f22a Check packfile protocole scheme + rm semicolons 2021-09-20 08:58:20 +02:00
S1m
5d3c7d1e2f Allow using external index.json and stickerpack 2021-09-19 17:35:28 +02:00
Tulir Asokan
ec8eeeeaf5 Fix scrolling topbar on Firefox
(and possibly break it on other browsers)
2021-04-22 19:12:54 +03:00
Tulir Asokan
57fde6fcad Assume https if homeserver URL doesn't have protocol 2021-04-22 19:12:54 +03:00
celogeek
9443e79e97 fix display of packs with sticker-import --list 2021-02-07 17:52:55 +01:00
celogeek
85813b22e5 add missing msgtype = m.sticker
On iOS the message is sent twice, with a duplicate event_id.
It cause error on logs, in different places (synapse, mautrix, ...)

It required to fix the already existing json or reimport the stickers.

The "packs/scalar*" example already include this field, and it works.
2021-02-07 17:50:50 +01:00
Andreas Weinzierl
569d9815c6 remove redundant utf-8 setting 2021-01-28 00:06:58 +01:00
Andreas Weinzierl
0f7b678f57 Use utf8-encoding whenever JSON is processed 2021-01-27 23:31:33 +01:00
Andreas Weinzierl
b884a9c387 Set encoding to utf-8 when saving json file for stickerpack
Fixes UnicodeEncodeError with Windows 10 when trying to import sticker packs caused by the default encoding scheme in Windows
2021-01-27 22:09:15 +01:00
Tulir Asokan
ba0096275c Update README.md 2021-01-24 13:14:59 +02:00
Tulir Asokan
3916ade97b Merge pull request #26 from auscompgeek/patch-1
Show sticker body in hover tooltip
2021-01-20 23:58:00 +02:00
Tulir Asokan
dab2420cd4 Merge pull request #28 from SpiritCroc/fix-import
Rename import.py, since import is a keyword
2021-01-20 23:57:28 +02:00
SpiritCroc
601d2acc32 Rename import.py, since import is a keyword
from sticker.import import cmd
                 ^
SyntaxError: invalid syntax

Fixes https://github.com/maunium/stickerpicker/issues/27.
2021-01-19 17:12:06 +01:00
David Vo
21d4f5cce6 Show sticker body in hover tooltip 2020-12-30 19:56:31 +11:00
Kévin Cocchi
9350d5f27b Update sass style file 2020-12-17 09:31:29 +01:00
Kévin Cocchi
add27513fe Implement stickers search 2020-12-17 01:01:32 +01:00
Kévin Cocchi
66d5b90ea1 Re-format CSS for readability in PR 2020-12-17 01:00:22 +01:00
19 changed files with 568 additions and 2237 deletions

View File

@@ -11,8 +11,14 @@ For setup and usage instructions, please visit the [wiki](https://github.com/mau
* [Enabling the widget](https://github.com/maunium/stickerpicker/wiki/Enabling-the-widget)
* [Hosting on GitHub pages](https://github.com/maunium/stickerpicker/wiki/Hosting-on-GitHub-pages)
If you prefer video tutorials, [Brodie Robertson](https://www.youtube.com/c/BrodieRobertson) has made a great video on setting up the picker and creating some packs: https://youtu.be/Yz3H6KJTEI0.
## Comparison with other sticker pickers
* Scalar is the default integration manager in Element, which can't be self-hosted and only supports predefined sticker packs.
* [Dimension](https://github.com/turt2live/matrix-dimension) is an alternate integration manager. It can be self-hosted, but it's more difficult than Maunium sticker picker.
* Maunium sticker picker is just a sticker picker rather than a full integration manager. It's much simpler than integration managers, but currently has to be set up manually per-user.
| Feature | Scalar | Dimension | Maunium sticker picker |
|---------------------------------|--------|-----------|------------------------|
| Free software | ❌ | ✔️ | ✔️ |

View File

@@ -45,9 +45,10 @@ setuptools.setup(
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
],
entry_points={"console_scripts": [
"sticker-import=sticker.import:cmd",
"sticker-import=sticker.stickerimport:cmd",
"sticker-pack=sticker.pack:cmd",
]},
)

View File

@@ -42,6 +42,7 @@ if TYPE_CHECKING:
url: str
info: MediaInfo
id: str
msgtype: str
else:
MediaInfo = None
StickerInfo = None
@@ -54,17 +55,29 @@ async def load_config(path: str) -> None:
config = json.load(config_file)
homeserver_url = config["homeserver"]
access_token = config["access_token"]
try:
giphy_api_key = config["giphy_api_key"]
giphy_mxc_prefix = config["giphy_mxc_prefix"]
except KeyError:
# these two are not mandatory, assume GIF search is disabled
print("Giphy related parameters not found in the config file.")
except FileNotFoundError:
print("Matrix config file not found. Please enter your homeserver and access token.")
print("Matrix config file not found. Please enter your homeserver and access token. Enter the Giphy API token if required, leave blank to disable the gif picker.")
homeserver_url = input("Homeserver URL: ")
access_token = input("Access token: ")
giphy_api_key = input("Giphy API key: ")
giphy_mxc_prefix = input("Giphy MXC prefix. Defaults to 'mxc://giphy.mau.dev/', required to proxy GIFs: ")
whoami_url = URL(homeserver_url) / "_matrix" / "client" / "r0" / "account" / "whoami"
if whoami_url.scheme not in ("https", "http"):
whoami_url = whoami_url.with_scheme("https")
user_id = await whoami(whoami_url, access_token)
with open(path, "w") as config_file:
json.dump({
"homeserver": homeserver_url,
"user_id": user_id,
"access_token": access_token
"access_token": access_token,
"giphy_api_key": giphy_api_key,
"giphy_mxc_prefix": giphy_mxc_prefix,
}, config_file)
print(f"Wrote config to {path}")

View File

@@ -13,6 +13,7 @@
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from functools import partial
from io import BytesIO
import os.path
import json
@@ -21,6 +22,7 @@ from PIL import Image
from . import matrix
open_utf8 = partial(open, encoding='UTF-8')
def convert_image(data: bytes) -> (bytes, int, int):
image: Image.Image = Image.open(BytesIO(data)).convert("RGBA")
@@ -41,7 +43,7 @@ def convert_image(data: bytes) -> (bytes, int, int):
def add_to_index(name: str, output_dir: str) -> None:
index_path = os.path.join(output_dir, "index.json")
try:
with open(index_path) as index_file:
with open_utf8(index_path) as index_file:
index_data = json.load(index_file)
except (FileNotFoundError, json.JSONDecodeError):
index_data = {"packs": []}
@@ -49,7 +51,7 @@ def add_to_index(name: str, output_dir: str) -> None:
index_data["homeserver_url"] = matrix.homeserver_url
if name not in index_data["packs"]:
index_data["packs"].append(name)
with open(index_path, "w") as index_file:
with open_utf8(index_path, "w") as index_file:
json.dump(index_data, index_file, indent=" ")
print(f"Added {name} to {index_path}")
@@ -74,4 +76,5 @@ def make_sticker(mxc: str, width: int, height: int, size: int,
"mimetype": "image/png",
},
},
"msgtype": "m.sticker",
}

View File

@@ -93,7 +93,7 @@ async def main(args: argparse.Namespace) -> None:
dirname = os.path.basename(os.path.abspath(args.path))
meta_path = os.path.join(args.path, "pack.json")
try:
with open(meta_path) as pack_file:
with util.open_utf8(meta_path) as pack_file:
pack = json.load(pack_file)
print(f"Loaded existing pack meta from {meta_path}")
except FileNotFoundError:
@@ -112,14 +112,14 @@ async def main(args: argparse.Namespace) -> None:
if sticker:
pack["stickers"].append(sticker)
with open(meta_path, "w") as pack_file:
with util.open_utf8(meta_path, "w") as pack_file:
json.dump(pack, pack_file)
print(f"Wrote pack to {meta_path}")
if args.add_to_index:
picker_file_name = f"{pack['id']}.json"
picker_pack_path = os.path.join(args.add_to_index, picker_file_name)
with open(picker_pack_path, "w") as pack_file:
with util.open_utf8(picker_pack_path, "w") as pack_file:
json.dump(pack, pack_file)
print(f"Copied pack to {picker_pack_path}")
util.add_to_index(picker_file_name, args.add_to_index)

View File

@@ -19,12 +19,12 @@ import json
index_path = "../web/packs/index.json"
try:
with open(index_path) as index_file:
with util.open_utf8(index_path) as index_file:
index_data = json.load(index_file)
except (FileNotFoundError, json.JSONDecodeError):
index_data = {"packs": []}
with open(sys.argv[-1]) as file:
with util.open_utf8(sys.argv[-1]) as file:
data = json.load(file)
for pack in data["assets"]:
@@ -45,12 +45,12 @@ for pack in data["assets"]:
}
filename = f"scalar-{pack['name'].replace(' ', '_')}.json"
pack_path = f"web/packs/{filename}"
with open(pack_path, "w") as pack_file:
with util.open_utf8(pack_path, "w") as pack_file:
json.dump(pack_data, pack_file)
print(f"Wrote {title} to {pack_path}")
if filename not in index_data["packs"]:
index_data["packs"].append(filename)
with open(index_path, "w") as index_file:
with util.open_utf8(index_path, "w") as index_file:
json.dump(index_data, index_file, indent=" ")
print(f"Updated {index_path}")

View File

@@ -71,7 +71,7 @@ async def reupload_pack(client: TelegramClient, pack: StickerSetFull, output_dir
already_uploaded = {}
try:
with open(pack_path) as pack_file:
with util.open_utf8(pack_path) as pack_file:
existing_pack = json.load(pack_file)
already_uploaded = {int(sticker["net.maunium.telegram.sticker"]["id"]): sticker
for sticker in existing_pack["stickers"]}
@@ -99,7 +99,7 @@ async def reupload_pack(client: TelegramClient, pack: StickerSetFull, output_dir
doc["body"] = sticker.emoticon
doc["net.maunium.telegram.sticker"]["emoticons"].append(sticker.emoticon)
with open(pack_path, "w") as pack_file:
with util.open_utf8(pack_path, "w") as pack_file:
json.dump({
"title": pack.set.title,
"id": f"tg-{pack.set.id}",
@@ -138,11 +138,12 @@ async def main(args: argparse.Namespace) -> None:
if args.list:
stickers: AllStickers = await client(GetAllStickersRequest(hash=0))
index = 1
width = len(str(stickers.sets))
width = len(str(len(stickers.sets)))
print("Your saved sticker packs:")
for saved_pack in stickers.sets:
print(f"{index:>{width}}. {saved_pack.title} "
f"(t.me/addstickers/{saved_pack.short_name})")
index += 1
elif args.pack[0]:
input_packs = []
for pack_url in args.pack[0]:
@@ -152,7 +153,7 @@ async def main(args: argparse.Namespace) -> None:
return
input_packs.append(InputStickerSetShortName(short_name=match.group(1)))
for input_pack in input_packs:
pack: StickerSetFull = await client(GetStickerSetRequest(input_pack))
pack: StickerSetFull = await client(GetStickerSetRequest(input_pack, hash=0))
await reupload_pack(client, pack, args.output_dir)
else:
parser.print_help()

23
web/esinstall.js Normal file
View File

@@ -0,0 +1,23 @@
const { install, printStats } = require("esinstall")
install(
[{
specifier: "htm/preact",
all: false,
default: false,
namespace: false,
named: ["html", "render", "Component"],
}],
{
dest: "./lib",
sourceMap: false,
treeshake: true,
verbose: true,
}
).then(data => {
const oldPrefix = "web_modules/"
const newPrefix = "lib/"
const spaces = " ".repeat(oldPrefix.length - newPrefix.length)
console.log("Installation complete")
console.log(printStats(data.stats).replace(oldPrefix, newPrefix + spaces))
})

View File

@@ -4,7 +4,6 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, user-scalable=no">
<title>Maunium sticker picker</title>
<link rel="modulepreload" href="src/widget-api.js"/>
<link rel="modulepreload" href="src/frequently-used.js"/>
<link rel="modulepreload" href="src/spinner.js"/>

File diff suppressed because one or more lines are too long

View File

@@ -4,28 +4,16 @@
"description": "A fast and simple Matrix sticker picker widget",
"repository": "https://github.com/maunium/stickerpicker",
"author": "Tulir Asokan <tulir@maunium.net>",
"license": "MPL-2.0",
"license": "AGPL-3.0-or-later",
"private": true,
"scripts": {
"snowpack": "snowpack",
"sass": "node-sass -o style style/*.sass --output-style compressed"
},
"snowpack": {
"install": [
"htm/preact"
],
"installOptions": {
"sourceMap": false,
"dest": "lib",
"treeshake": true
}
"esinstall": "node ./esinstall.js",
"sass": "sass --no-source-map --style=compressed style/"
},
"dependencies": {
"htm": "^3.0.4",
"preact": "^10.4.8",
"snowpack": "^2.10.3"
},
"devDependencies": {
"node-sass": "^4.14.1"
"htm": "^3.1.0",
"preact": "^10.5.14",
"esinstall": "^1.1.7",
"sass": "^1.42.1"
}
}

1
web/res/search.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M9.145 18.29c-5.042 0-9.145-4.102-9.145-9.145s4.103-9.145 9.145-9.145 9.145 4.103 9.145 9.145-4.102 9.145-9.145 9.145zm0-15.167c-3.321 0-6.022 2.702-6.022 6.022s2.702 6.022 6.022 6.022 6.023-2.702 6.023-6.022-2.702-6.022-6.023-6.022zm9.263 12.443c-.817 1.176-1.852 2.188-3.046 2.981l5.452 5.453 3.014-3.013-5.42-5.421z"/></svg>

After

Width:  |  Height:  |  Size: 419 B

View File

@@ -15,14 +15,24 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import { html, render, Component } from "../lib/htm/preact.js"
import { Spinner } from "./spinner.js"
import { SearchBox } from "./search-box.js"
import * as widgetAPI from "./widget-api.js"
import * as frequent from "./frequently-used.js"
// import GiphyAPI from "./GiphySearch.js"
// The base URL for fetching packs. The app will first fetch ${PACK_BASE_URL}/index.json,
// then ${PACK_BASE_URL}/${packFile} for each packFile in the packs object of the index.json file.
const PACKS_BASE_URL = "packs"
let INDEX = `${PACKS_BASE_URL}/index.json`
const params = new URLSearchParams(document.location.search)
if (params.has('config')) {
INDEX = params.get("config")
}
// This is updated from packs/index.json
let HOMESERVER_URL = "https://matrix-client.matrix.org"
let GIPHY_API_KEY = ""
let GIPHY_MXC_PREFIX = "mxc://giphy.mau.dev/"
const makeThumbnailURL = mxc => `${HOMESERVER_URL}/_matrix/media/r0/thumbnail/${mxc.substr(6)}?height=128&width=128&method=scale`
@@ -30,19 +40,113 @@ const makeThumbnailURL = mxc => `${HOMESERVER_URL}/_matrix/media/r0/thumbnail/${
// This is also used to fix scrolling to sections on Element iOS
const isMobileSafari = navigator.userAgent.match(/(iPod|iPhone|iPad)/) && navigator.userAgent.match(/AppleWebKit/)
export const parseQuery = str => Object.fromEntries(
str.split("&")
.map(part => part.split("="))
.map(([key, value = ""]) => [key, value]))
const supportedThemes = ["light", "dark", "black"]
const defaultState = {
packs: [],
filtering: {
searchTerm: "",
packs: [],
},
}
class GiphySearchTab extends Component {
constructor(props) {
super(props);
this.state = {
searchTerm: "",
gifs: [],
loading: false,
GIFById: {},
};
this.handleSearchChange = this.handleSearchChange.bind(this);
this.searchGifs = this.searchGifs.bind(this);
this.handleGifClick = this.handleGifClick.bind(this);
}
async searchGifs() {
this.setState({ loading: true });
try {
const apiKey = GIPHY_API_KEY;
const mxc_prefix = GIPHY_MXC_PREFIX;
const url = `https://api.giphy.com/v1/gifs/search?q=${this.state.searchTerm}&api_key=${apiKey}`;
this.setState({ loading: true });
const response = await fetch(url);
const data = await response.json();
this.setState({ gifs: data.data, loading: false });
data.data.forEach((jsonElement) => {
const id = jsonElement.id;
const updatedItem = {
"body": jsonElement.title,
"info": {
"h": jsonElement.images.original.height,
"w": jsonElement.images.original.width,
"size": jsonElement.images.original.size,
"mimetype": "image/webp",
},
"msgtype": "m.image",
"url": mxc_prefix+jsonElement.id
};
this.setState((prevState) => ({
GIFById: {...prevState.GIFById, [id]: updatedItem}}));
});
} catch (error) {
this.setState({ error: "Error fetching GIFs", loading: false });
this.setState({ loading: false });
}
}
handleSearchChange(event) {
this.setState({ searchTerm: event.target.value });
}
handleGifClick(gif) {
console.log(this.state.GIFById[gif.id]);
widgetAPI.sendGIF(this.state.GIFById[gif.id]);
}
async searchGiphy(searchTerm) {
if (!searchTerm) return;
};
render() {
const { searchTerm, gifs, loading } = this.state;
return html`
<div class="search-box">
<input
type="text"
value=${searchTerm}
onInput=${this.handleSearchChange}
placeholder="Search GIFs..."
/>
<button onClick=${this.searchGifs} disabled=${loading}>Search</button>
</div>
<!-- <div class="gifs-list" style="display: grid"> -->
<div class="pack-list">
<section class="stickerpack">
<div class="sticker-list">
${GIPHY_API_KEY !== "" && gifs.map((gif) => html`
<div class="sticker" onClick=${() => this.handleGifClick(gif)} data-gif-id=${gif.id}>
<img src=${gif.images.fixed_height.url} alt=${gif.title} class="visible" data=/>
</div>
`)}
</div>
</div>
</div>
`;
}
}
class App extends Component {
constructor(props) {
super(props)
this.defaultTheme = parseQuery(location.search.substr(1)).theme
this.defaultTheme = params.get("theme")
this.state = {
packs: [],
activeTab: "stickers",
packs: defaultState.packs,
loading: true,
error: null,
stickersPerRow: parseInt(localStorage.mauStickersPerRow || "4"),
@@ -53,6 +157,7 @@ class App extends Component {
stickerIDs: frequent.get(),
stickers: [],
},
filtering: defaultState.filtering,
}
if (!supportedThemes.includes(this.state.theme)) {
this.state.theme = "light"
@@ -65,6 +170,7 @@ class App extends Component {
this.imageObserver = null
this.packListRef = null
this.navRef = null
this.searchStickers = this.searchStickers.bind(this)
this.sendSticker = this.sendSticker.bind(this)
this.navScroll = this.navScroll.bind(this)
this.reloadPacks = this.reloadPacks.bind(this)
@@ -89,6 +195,28 @@ class App extends Component {
localStorage.mauFrequentlyUsedStickerCache = JSON.stringify(stickers.map(sticker => [sticker.id, sticker]))
}
searchStickers(e) {
const sanitizeString = s => s.toLowerCase().trim()
const searchTerm = sanitizeString(e.target.value)
const allPacks = [this.state.frequentlyUsed, ...this.state.packs]
const packsWithFilteredStickers = allPacks.map(pack => ({
...pack,
stickers: pack.stickers.filter(sticker =>
sanitizeString(sticker.body).includes(searchTerm) ||
sanitizeString(sticker.id).includes(searchTerm)
),
}))
this.setState({
filtering: {
...this.state.filtering,
searchTerm,
packs: packsWithFilteredStickers.filter(({ stickers }) => !!stickers.length),
},
})
}
setStickersPerRow(val) {
localStorage.mauStickersPerRow = val
document.documentElement.style.setProperty("--stickers-per-row", localStorage.mauStickersPerRow)
@@ -111,13 +239,16 @@ class App extends Component {
reloadPacks() {
this.imageObserver.disconnect()
this.sectionObserver.disconnect()
this.setState({ packs: [] })
this.setState({
packs: defaultState.packs,
filtering: defaultState.filtering,
})
this._loadPacks(true)
}
_loadPacks(disableCache = false) {
const cache = disableCache ? "no-cache" : undefined
fetch(`${PACKS_BASE_URL}/index.json`, { cache }).then(async indexRes => {
fetch(INDEX, { cache }).then(async indexRes => {
if (indexRes.status >= 400) {
this.setState({
loading: false,
@@ -127,9 +258,16 @@ class App extends Component {
}
const indexData = await indexRes.json()
HOMESERVER_URL = indexData.homeserver_url || HOMESERVER_URL
GIPHY_API_KEY = indexData.giphy_api_key || ""
GIPHY_MXC_PREFIX = indexData.giphy_mxc_prefix || GIPHY_MXC_PREFIX
// TODO only load pack metadata when scrolled into view?
for (const packFile of indexData.packs) {
const packRes = await fetch(`${PACKS_BASE_URL}/${packFile}`, { cache })
let packRes
if (packFile.startsWith("https://") || packFile.startsWith("http://")) {
packRes = await fetch(packFile, { cache })
} else {
packRes = await fetch(`${PACKS_BASE_URL}/${packFile}`, { cache })
}
const packData = await packRes.json()
for (const sticker of packData.stickers) {
this.stickersByID.set(sticker.id, sticker)
@@ -220,34 +358,58 @@ class App extends Component {
}
navScroll(evt) {
this.navRef.scrollLeft += evt.deltaY * 12
this.navRef.scrollLeft += evt.deltaY
}
render() {
const theme = `theme-${this.state.theme}`
if (this.state.loading) {
return html`<main class="spinner ${theme}"><${Spinner} size=${80} green /></main>`
} else if (this.state.error) {
return html`<main class="error ${theme}">
<h1>Failed to load packs</h1>
<p>${this.state.error}</p>
</main>`
} else if (this.state.packs.length === 0) {
return html`<main class="empty ${theme}"><h1>No packs found 😿</h1></main>`
}
return html`<main class="has-content ${theme}">
<nav onWheel=${this.navScroll} ref=${elem => this.navRef = elem}>
<${NavBarItem} pack=${this.state.frequentlyUsed} iconOverride="recent" />
${this.state.packs.map(pack => html`<${NavBarItem} id=${pack.id} pack=${pack}/>`)}
<${NavBarItem} pack=${{ id: "settings", title: "Settings" }} iconOverride="settings" />
</nav>
<div class="pack-list ${isMobileSafari ? "ios-safari-hack" : ""}" ref=${elem => this.packListRef = elem}>
<${Pack} pack=${this.state.frequentlyUsed} send=${this.sendSticker} />
${this.state.packs.map(pack => html`<${Pack} id=${pack.id} pack=${pack} send=${this.sendSticker} />`)}
<${Settings} app=${this}/>
</div>
</main>`
}
const theme = `theme-${this.state.theme}`;
const filterActive = !!this.state.filtering.searchTerm;
const packs = filterActive
? this.state.filtering.packs
: [this.state.frequentlyUsed, ...this.state.packs];
if (this.state.loading) {
return html`<main class="spinner ${theme}"><${Spinner} size=${80} green /></main>`;
} else if (this.state.error) {
return html`<main class="error ${theme}">
<h1>Failed to load packs</h1>
<p>${this.state.error}</p>
</main>`;
} else if (this.state.packs.length === 0) {
return html`<main class="empty ${theme}"><h1>No packs found 😿</h1></main>`;
}
return html`<main class="has-content ${theme}">
<div class="tab-container" style="display: flex;">
<a href="#stickers" class="tab" onClick=${() => this.setState({ activeTab: "stickers" })}>Stickers</a>
<a href="#gifs" class="tab" onClick=${() => this.setState({ activeTab: "gifs" })}>GIFs</a>
</div>
${this.state.activeTab === "stickers" && html`
<nav onWheel=${this.navScroll} ref=${elem => this.navRef = elem}>
<${NavBarItem} pack=${this.state.frequentlyUsed} iconOverride="recent" />
${this.state.packs.map(pack => html`<${NavBarItem} id=${pack.id} pack=${pack}/>`)}
<${NavBarItem} pack=${{ id: "settings", title: "Settings" }} iconOverride="settings" />
</nav>
<${SearchBox} onKeyUp=${this.searchStickers} />
<div class="pack-list ${isMobileSafari ? "ios-safari-hack" : ""}" ref=${(elem) => (this.packListRef = elem)}>
${filterActive && packs.length === 0
? html`<div class="search-empty"><h1>No stickers match your search</h1></div>`
: null}
${packs.map((pack) => html`<${Pack} id=${pack.id} pack=${pack} send=${this.sendSticker} />`)}
<${Settings} app=${this} />
</div>
`}
${this.state.activeTab === "gifs" && GIPHY_API_KEY !== "" && html`
<${GiphySearchTab} send=${this.sendGIF} />
`}
${this.state.activeTab === "gifs" && GIPHY_API_KEY === "" && html`
<h1><center>GIF Search is not enabled. Please enable it in the config.</center></h1>
`}
</main>`;
}
}
const Settings = ({ app }) => html`
@@ -309,7 +471,7 @@ const Pack = ({ pack, send }) => html`
const Sticker = ({ content, send }) => html`
<div class="sticker" onClick=${send} data-sticker-id=${content.id}>
<img data-src=${makeThumbnailURL(content.url)} alt=${content.body} />
<img data-src=${makeThumbnailURL(content.url)} alt=${content.body} title=${content.body} />
</div>
`

26
web/src/search-box.js Normal file
View File

@@ -0,0 +1,26 @@
// maunium-stickerpicker - A fast and simple Matrix sticker picker widget.
// Copyright (C) 2020 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import { html } from "../lib/htm/preact.js"
export const SearchBox = ({ onKeyUp, placeholder = 'Find stickers' }) => {
const component = html`
<div class="search-box">
<input type="text" placeholder=${placeholder} onKeyUp=${onKeyUp} />
<span class="icon icon-search" />
</div>
`
return component
}

View File

@@ -74,3 +74,7 @@ export function sendSticker(content) {
widgetData,
}, "*")
}
export function sendGIF(content) {
return sendSticker(content)
}

View File

@@ -1 +1 @@
*{font-family:sans-serif}body{margin:0}h1{font-size:1rem}:root{--stickers-per-row: 4;--sticker-size: calc(100vw / var(--stickers-per-row))}main{color:var(--text-color)}main.spinner{margin-top:5rem}main.error,main.empty{margin:2rem}main.empty{text-align:center}main.has-content{position:fixed;top:0;left:0;right:0;bottom:0;display:grid;grid-template-rows:calc(12vw + 2px) auto}main.theme-light{--highlight-color: #eee;--text-color: black;background-color:white}main.theme-dark{--highlight-color: #444;--text-color: white;background-color:#22262e}main.theme-black{--highlight-color: #222;--text-color: white;background-color:black}.icon{width:100%;height:100%;background-color:var(--text-color);mask-size:contain;-webkit-mask-size:contain;mask-image:var(--icon-image);-webkit-mask-image:var(--icon-image)}.icon.icon-settings{--icon-image: url(../res/settings.svg)}.icon.icon-recent{--icon-image: url(../res/recent.svg)}nav{display:flex;overflow-x:auto}nav>a{border-bottom:2px solid transparent}nav>a.visible{border-bottom-color:green}nav>a>div.sticker{width:12vw;height:12vw}div.pack-list,nav{scrollbar-width:none}div.pack-list::-webkit-scrollbar,nav::-webkit-scrollbar{display:none}div.pack-list{overflow-y:auto}div.pack-list.ios-safari-hack{position:fixed;top:calc(12vw + 2px);bottom:0;left:0;right:0;-webkit-overflow-scrolling:touch}section.stickerpack{margin-top:.75rem}section.stickerpack>div.sticker-list{display:flex;flex-wrap:wrap}section.stickerpack>h1{margin:0 0 0 .75rem}div.sticker{display:flex;padding:4px;cursor:pointer;position:relative;width:var(--sticker-size);height:var(--sticker-size);box-sizing:border-box}div.sticker:hover{background-color:var(--highlight-color)}div.sticker>img{display:none;width:100%;object-fit:contain}div.sticker>img.visible{display:initial}div.sticker>.icon{width:70%;height:70%;margin:15%}div.settings-list{display:flex;flex-direction:column}div.settings-list>*{margin:.5rem}div.settings-list button{padding:.5rem;border-radius:.25rem}div.settings-list input{width:100%}
*{font-family:sans-serif}body{margin:0}h1{font-size:1rem}:root{--stickers-per-row: 4;--sticker-size: calc(100vw / var(--stickers-per-row))}main{color:var(--text-color)}main.spinner{margin-top:5rem}main.error,main.empty{margin:2rem}main.empty{text-align:center}main.has-content{position:fixed;top:0;left:0;right:0;bottom:0;display:grid;grid-template-rows:calc(12vw + 2px) min-content auto}main.theme-light{--highlight-color: #eee;--search-box-color: var(--highlight-color);--text-color: black;background-color:#fff}main.theme-dark{--highlight-color: #444;--search-box-color: #383e4b;--text-color: white;background-color:#22262e}main.theme-black{--highlight-color: #222;--search-box-color: var(--highlight-color);--text-color: white;background-color:#000}.icon{width:100%;height:100%;background-color:var(--text-color);mask-size:contain;-webkit-mask-size:contain;mask-image:var(--icon-image);-webkit-mask-image:var(--icon-image)}.icon.icon-settings{--icon-image: url(../res/settings.svg)}.icon.icon-recent{--icon-image: url(../res/recent.svg)}.icon.icon.icon-search{--icon-image: url(../res/search.svg)}nav{display:flex;overflow-x:auto}nav>a{border-bottom:2px solid rgba(0,0,0,0)}nav>a.visible{border-bottom-color:green}nav>a>div.sticker{width:12vw;height:12vw}div.pack-list,nav{scrollbar-width:none}div.pack-list::-webkit-scrollbar,nav::-webkit-scrollbar{display:none}div.pack-list{overflow-y:auto}div.pack-list.ios-safari-hack{position:fixed;top:calc(calc(12vw + 2px) + calc(2 * 0.7rem + 2 * 0.5rem + 1rem));bottom:0;left:0;right:0;-webkit-overflow-scrolling:touch}div.search-empty{margin:1.2rem;text-align:center}section.stickerpack{margin-top:.75rem}section.stickerpack>div.sticker-list{display:flex;flex-wrap:wrap}section.stickerpack>h1{margin:0 0 0 .75rem}div.sticker{display:flex;padding:4px;cursor:pointer;position:relative;width:var(--sticker-size);height:var(--sticker-size);box-sizing:border-box}div.sticker:hover{background-color:var(--highlight-color)}div.sticker>img{display:none;width:100%;object-fit:contain}div.sticker>img.visible{display:initial}div.sticker>.icon{width:70%;height:70%;margin:15%}div.search-box{position:relative;display:flex}div.search-box>input[type=text]{flex-grow:1;background-color:var(--search-box-color);outline:none;border:none;border-radius:.25rem;height:1rem;padding:.7rem;padding-right:calc(1rem + 0.7rem);margin:.5rem;font-size:1rem;color:var(--text-color)}div.search-box>span.icon{display:flex;position:absolute;top:calc(50% - 1rem/2);right:1rem;width:1rem;height:1rem;box-sizing:border-box}div.settings-list{display:flex;flex-direction:column}div.settings-list>*{margin:.5rem}div.settings-list button{padding:.5rem;border-radius:.25rem}div.settings-list input{width:100%}a.tab{padding:5% 5%;width:40%;text-align:center;border:none;background-color:#f0f0f0;cursor:pointer;-webkit-appearance:button;-moz-appearance:button;appearance:button;text-decoration:none;color:initial}

View File

@@ -32,6 +32,12 @@ $nav-bottom-highlight: 2px
$nav-height: calc(#{$nav-sticker-size} + #{$nav-bottom-highlight})
$nav-height-inverse: calc(-#{$nav-sticker-size} - #{$nav-bottom-highlight})
$search-box-icon-size: 1rem
$search-box-input-height: 1rem
$search-box-input-padding: .7rem
$search-box-input-margin: .5rem
$search-box-height: calc(2 * #{$search-box-input-padding} + 2 * #{$search-box-input-margin} + #{$search-box-input-height})
main
color: var(--text-color)
@@ -50,22 +56,24 @@ main
left: 0
right: 0
bottom: 0
display: grid
grid-template-rows: $nav-height auto
grid-template-rows: $nav-height min-content auto
main.theme-light
--highlight-color: #eee
--search-box-color: var(--highlight-color)
--text-color: black
background-color: white
main.theme-dark
--highlight-color: #444
--search-box-color: #383e4b
--text-color: white
background-color: #22262e
main.theme-black
--highlight-color: #222
--search-box-color: var(--highlight-color)
--text-color: white
background-color: black
@@ -84,6 +92,9 @@ main.theme-black
&.icon-recent
--icon-image: url(../res/recent.svg)
&.icon.icon-search
--icon-image: url(../res/search.svg)
nav
display: flex
overflow-x: auto
@@ -109,12 +120,16 @@ div.pack-list
div.pack-list.ios-safari-hack
position: fixed
top: $nav-height
top: calc(#{$nav-height} + #{$search-box-height})
bottom: 0
left: 0
right: 0
-webkit-overflow-scrolling: touch
div.search-empty
margin: 1.2rem
text-align: center
section.stickerpack
margin-top: .75rem
@@ -150,6 +165,32 @@ div.sticker
height: 70%
margin: 15%
div.search-box
position: relative
display: flex
>input[type="text"]
flex-grow: 1
background-color: var(--search-box-color)
outline: none
border: none
border-radius: .25rem
height: $search-box-input-height
padding: $search-box-input-padding
padding-right: calc(#{$search-box-icon-size} + #{$search-box-input-padding})
margin: $search-box-input-margin
font-size: 1rem
color: var(--text-color)
>span.icon
display: flex
position: absolute
top: calc(50% - #{$search-box-icon-size} / 2)
right: $search-box-icon-size
width: $search-box-icon-size
height: $search-box-icon-size
box-sizing: border-box
div.settings-list
display: flex
flex-direction: column
@@ -163,3 +204,16 @@ div.settings-list
input
width: 100%
a.tab
padding: 5% 5%
width: 40%
text-align: center
border: none
background-color: #f0f0f0
cursor: pointer
-webkit-appearance: button
-moz-appearance: button
appearance: button
text-decoration: none
color: initial

View File

@@ -1 +1 @@
.sk-center-wrapper{width:100%;display:flex;justify-content:space-around}.sk-chase{position:relative;animation:sk-chase 2.5s infinite linear both}.sk-chase.green>.sk-chase-dot:before{background-color:#00C853}.sk-chase>.sk-chase-dot{width:100%;height:100%;position:absolute;left:0;top:0;animation:sk-chase-dot 2.0s infinite ease-in-out both}.sk-chase>.sk-chase-dot:before{content:'';display:block;width:25%;height:25%;border-radius:100%;animation:sk-chase-dot-before 2.0s infinite ease-in-out both;background-color:#FFF}.sk-chase>.sk-chase-dot:nth-child(1){animation-delay:-1.1s}.sk-chase>.sk-chase-dot:nth-child(2){animation-delay:-1.0s}.sk-chase>.sk-chase-dot:nth-child(3){animation-delay:-0.9s}.sk-chase>.sk-chase-dot:nth-child(4){animation-delay:-0.8s}.sk-chase>.sk-chase-dot:nth-child(5){animation-delay:-0.7s}.sk-chase>.sk-chase-dot:nth-child(6){animation-delay:-0.6s}.sk-chase>.sk-chase-dot:nth-child(1):before{animation-delay:-1.1s}.sk-chase>.sk-chase-dot:nth-child(2):before{animation-delay:-1.0s}.sk-chase>.sk-chase-dot:nth-child(3):before{animation-delay:-0.9s}.sk-chase>.sk-chase-dot:nth-child(4):before{animation-delay:-0.8s}.sk-chase>.sk-chase-dot:nth-child(5):before{animation-delay:-0.7s}.sk-chase>.sk-chase-dot:nth-child(6):before{animation-delay:-0.6s}@keyframes sk-chase{100%{transform:rotate(360deg)}}@keyframes sk-chase-dot{80%,100%{transform:rotate(360deg)}}@keyframes sk-chase-dot-before{50%{transform:scale(0.4)}100%,0%{transform:scale(1)}}
.sk-center-wrapper{width:100%;display:flex;justify-content:space-around}.sk-chase{position:relative;animation:sk-chase 2.5s infinite linear both}.sk-chase.green>.sk-chase-dot:before{background-color:#00c853}.sk-chase>.sk-chase-dot{width:100%;height:100%;position:absolute;left:0;top:0;animation:sk-chase-dot 2s infinite ease-in-out both}.sk-chase>.sk-chase-dot:before{content:"";display:block;width:25%;height:25%;border-radius:100%;animation:sk-chase-dot-before 2s infinite ease-in-out both;background-color:#fff}.sk-chase>.sk-chase-dot:nth-child(1){animation-delay:-1.1s}.sk-chase>.sk-chase-dot:nth-child(2){animation-delay:-1s}.sk-chase>.sk-chase-dot:nth-child(3){animation-delay:-0.9s}.sk-chase>.sk-chase-dot:nth-child(4){animation-delay:-0.8s}.sk-chase>.sk-chase-dot:nth-child(5){animation-delay:-0.7s}.sk-chase>.sk-chase-dot:nth-child(6){animation-delay:-0.6s}.sk-chase>.sk-chase-dot:nth-child(1):before{animation-delay:-1.1s}.sk-chase>.sk-chase-dot:nth-child(2):before{animation-delay:-1s}.sk-chase>.sk-chase-dot:nth-child(3):before{animation-delay:-0.9s}.sk-chase>.sk-chase-dot:nth-child(4):before{animation-delay:-0.8s}.sk-chase>.sk-chase-dot:nth-child(5):before{animation-delay:-0.7s}.sk-chase>.sk-chase-dot:nth-child(6):before{animation-delay:-0.6s}@keyframes sk-chase{100%{transform:rotate(360deg)}}@keyframes sk-chase-dot{80%,100%{transform:rotate(360deg)}}@keyframes sk-chase-dot-before{50%{transform:scale(0.4)}100%,0%{transform:scale(1)}}

File diff suppressed because it is too large Load Diff