Compare commits

...

7 Commits

Author SHA1 Message Date
IRHM 1e7702b16b docker/ui-build: Add doc link to readme 2025-02-28 23:50:56 +00:00
IRHM fa2a4a40d0 doc: Create docker-compose-subpath installation guide 2025-02-28 23:46:57 +00:00
IRHM 203e4a31ef doc: Remove docsVersionDropdown from nav
Was going to version doc, but never did and don't want to anymore.. doc site will only support latest version of watcharr.

If anyone needs an older version for any reason, can always browse through them in specific git tag.
2025-02-28 23:03:57 +00:00
IRHM 5f2c1977ee Create toBaseUrl and use everywhere we navigate
Simply prefixes the absolute paths we use for internal navigations with the `base` variable set when the app was built.
2025-02-28 22:48:34 +00:00
IRHM 5860fd2783 ui-build compose: Add name to volume
and use latest prod image in ui-replace compose test
2025-02-28 21:11:55 +00:00
IRHM 3c2cbf55c6 new ui-build image, WATCHARR_BASE env var for svelte cfg, rename compose files 2025-02-25 22:58:59 +00:00
Andreas 565d90dbf0 feat: new about modal (#811)
* FaceMenu: Add about modal

added modal with about info to FaceMenu bottom.
added 4 new SVG icons.

* FaceMenu: npm format

* FaceMenu: refactor about modal

- moved about modal into its own file
- minor fixes

* AboutModal: Add text to gh/site links, use Modal title prop, add attribution file link

, added a hover effect to all links in the modal and removed the margin on h5.

Also updated the top paragraph.

* AboutModal: Remove unused style

* Menu: Use color variables

---------

Co-authored-by: IRHM <37304121+IRHM@users.noreply.github.com>
2025-02-21 01:14:13 +00:00
41 changed files with 520 additions and 66 deletions
+3
View File
@@ -0,0 +1,3 @@
# Docker dev volume
container_data/
data/
+1 -1
View File
@@ -54,7 +54,7 @@ Feel free to abuse this demo instance (nicely), which runs on the latest `dev` b
# Set Up
[Checkout our documentation](https://watcharr.app/docs/category/installation) for an up to date guide on setup! If you hate manuals, but love docker, this [docker-compose.yml](./docker-compose.yml) file is your friend.
[Checkout our documentation](https://watcharr.app/docs/category/installation) for an up to date guide on setup! If you hate manuals, but love docker, this [compose.yml](./compose.yml) file is your friend.
# Community Made Tools
+24
View File
@@ -0,0 +1,24 @@
# This compose file will replace watcharrs
# built in UI with a custom one provided in
# a named volume (for use with ui-build image).
services:
watcharr:
# We dont need to build here too for testing if
# a custom ui works, so using latest prod image.
image: ghcr.io/sbondco/watcharr:latest
container_name: watcharr
ports:
- 3080:3080
volumes:
- ./container_data:/data
- type: volume
source: watcharr-ui
target: /ui
volume:
nocopy: true
subpath: build
volumes:
watcharr-ui:
external: true
View File
@@ -0,0 +1,104 @@
---
sidebar_position: 2
description: Install and setup with Docker Compose for access through a subpath.
---
# Docker Compose (subpath)
Install and setup with Docker Compose for access through a subpath.
:::info Not a great experience at the moment!
Currently, hosting Watcharr via a subpath on your server is not very easy (unlike hosting under a subdomain). Hopefully this will change in the future, but as a temporary measure to at least allow hosting under a subpath, this method has been provided.
The following issue will continue to track this: https://github.com/sbondCo/Watcharr/issues/312
:::
## Installing
### Build UI
First we have to build the frontend with our subpath provided as an environment variable.
If you don't want to use `/watcharr` as your subpath, replace the value of `WATCHARR_BASE` before running the command.
```bash
docker run -e WATCHARR_BASE=/watcharr -v watcharr-ui:/ui --rm ghcr.io/sbondco/watcharr-ui-build:latest
```
Once the container has finished running the build script, it will exit and remove itself. The `watcharr-ui` volume will contain the built files.
### Install Watcharr
Now we can install Watcharr. You can copy the example below to get started:
```yaml title="compose.yml"
services:
watcharr:
# The :latest tag is used for simplicity, it is recommended
# to use an actual version, then when updating check the releases for changelogs.
image: ghcr.io/sbondco/watcharr:latest
container_name: watcharr
ports:
- 3080:3080
volumes:
# Contains all of watcharr data (database & cache)
- ./data:/data
# Use our volume containing built ui files
# instead of default ui included in image.
- type: volume
source: watcharr-ui
target: /ui
volume:
nocopy: true
subpath: build
restart: unless-stopped
volumes:
watcharr-ui:
external: true
```
:::danger first account
When **first** running Watcharr, make sure only you have access. The first user created will become admin.
:::
You can now start `Watcharr` like so:
```bash
docker compose up -d
```
If you didn't change the ports in the example, the server will be available at [http://localhost:3080/](http://localhost:3080/).
## Updating
:::danger Take care
We try taking care as to not release breaking changes, however it is still recommended that
you lookover changelogs before updating!
Breaking changes are marked at the top of releases: https://github.com/sbondCo/Watcharr/releases
:::
1. Update your built ui files by following the [Build UI](#build-ui) step again.
2. Update the `image` version in your `compose.yml` file.
Skip this step if you are using the `latest` tag.
```yaml
# eg. update v1.19.0 to v1.20.0 (or whatever version you are updating to)
image: ghcr.io/sbondco/watcharr:v1.19.0
```
3. Pull the new changes and re-create your container:
```bash
docker compose pull && docker compose down && docker compose up -d
```
And that is it!
+2 -2
View File
@@ -9,7 +9,7 @@ description: Install and setup with Docker Compose.
Installing Watcharr with a docker compose file is easy. You can copy the example below to get started:
```yaml title="docker-compose.yml"
```yaml title="compose.yml"
services:
watcharr:
# The :latest tag is used for simplicity, it is recommended
@@ -51,7 +51,7 @@ Breaking changes are marked at the top of releases: https://github.com/sbondCo/W
Updating your server can be done in two steps:
1. Update the `image` version in your `docker-compose.yml` file.
1. Update the `image` version in your `compose.yml` file.
Skip this step if you are using the `latest` tag.
```yaml
-3
View File
@@ -80,9 +80,6 @@ const config = {
position: "left",
label: "Docs",
},
{
type: "docsVersionDropdown",
},
{
href: "https://beta.watcharr.app",
label: "Demo",
+3
View File
@@ -0,0 +1,3 @@
This folder is for any _extra_ images that we produce.
The main watcharr Dockerfile remains in project root.
+16
View File
@@ -0,0 +1,16 @@
#
# Frontend
#
FROM node:20-alpine AS ui
COPY ./docker/ui-build/entrypoint.sh /entrypoint.sh
RUN ["chmod", "+x", "/entrypoint.sh"]
WORKDIR /app
COPY package*.json vite.config.ts svelte.config.js tsconfig.json ./
COPY ./src ./src
COPY ./static ./static
VOLUME /ui
ENTRYPOINT ["/entrypoint.sh"]
+7
View File
@@ -0,0 +1,7 @@
Image for building watcharr ui.
Accepts environment variables:
- `WATCHARR_BASE`
https://watcharr.app/docs/installation/docker-compose-subpath
+15
View File
@@ -0,0 +1,15 @@
# To test prod
services:
watcharr-ui-build:
build:
context: ../../
dockerfile: ./docker/ui-build/Dockerfile
container_name: watcharr-ui-build
volumes:
- watcharr-ui:/ui
environment:
WATCHARR_BASE: "/sp"
volumes:
watcharr-ui:
name: "watcharr-ui"
+25
View File
@@ -0,0 +1,25 @@
#!/bin/sh
# This is loosely based on the main containers Dockerfile commands.
# Just everything needed to get ui built and ready and nothing else.
set -x
echo "wuib: Starting build"
# Install all deps
/usr/local/bin/npm install
# Run build
/usr/local/bin/npm run build
# Remove any existing build files from volume
rm -rf /ui/build
# Move build folder to /ui
mv /app/build /ui
# Move package.json and lock file to /ui/build for final npm ci call
mv /app/package.json /app/package-lock.json /ui/build
# cd to build files final destination and install dependencies for production
cd /ui/build && /usr/local/bin/npm ci --omit=dev --ignore-scripts=true
+48
View File
@@ -645,6 +645,54 @@
d="M368 192h-16v-80a96 96 0 10-192 0v80h-16a64.07 64.07 0 00-64 64v176a64.07 64.07 0 0064 64h224a64.07 64.07 0 0064-64V256a64.07 64.07 0 00-64-64zm-48 0H192v-80a64 64 0 11128 0z"
/>
</svg>
{:else if i === "github"}
<svg
xmlns="http://www.w3.org/2000/svg"
width={wh}
height={wh}
viewBox="0 0 24 25"
>
<path
fill="currentColor"
d="M12.301 0h.093c2.242 0 4.34.613 6.137 1.68l-.055-.031a12.35 12.35 0 0 1 4.449 4.422l.031.058a12.2 12.2 0 0 1 1.654 6.166c0 5.406-3.483 10-8.327 11.658l-.087.026a.72.72 0 0 1-.642-.113l.002.001a.62.62 0 0 1-.208-.466v-.014v.001l.008-1.226q.008-1.178.008-2.154a2.84 2.84 0 0 0-.833-2.274a11 11 0 0 0 1.718-.305l-.076.017a6.5 6.5 0 0 0 1.537-.642l-.031.017a4.5 4.5 0 0 0 1.292-1.058l.006-.007a4.9 4.9 0 0 0 .84-1.645l.009-.035a7.9 7.9 0 0 0 .329-2.281l-.001-.136v.007l.001-.072a4.73 4.73 0 0 0-1.269-3.23l.003.003c.168-.44.265-.948.265-1.479a4.25 4.25 0 0 0-.404-1.814l.011.026a2.1 2.1 0 0 0-1.31.181l.012-.005a8.6 8.6 0 0 0-1.512.726l.038-.022l-.609.384c-.922-.264-1.981-.416-3.075-.416s-2.153.152-3.157.436l.081-.02q-.256-.176-.681-.433a9 9 0 0 0-1.272-.595l-.066-.022A2.17 2.17 0 0 0 5.837 5.1l.013-.002a4.2 4.2 0 0 0-.393 1.788c0 .531.097 1.04.275 1.509l-.01-.029a4.72 4.72 0 0 0-1.265 3.303v-.004l-.001.13c0 .809.12 1.591.344 2.327l-.015-.057c.189.643.476 1.202.85 1.693l-.009-.013a4.4 4.4 0 0 0 1.267 1.062l.022.011c.432.252.933.465 1.46.614l.046.011c.466.125 1.024.227 1.595.284l.046.004c-.431.428-.718 1-.784 1.638l-.001.012a3 3 0 0 1-.699.236l-.021.004c-.256.051-.549.08-.85.08h-.066h.003a1.9 1.9 0 0 1-1.055-.348l.006.004a2.84 2.84 0 0 1-.881-.986l-.007-.015a2.6 2.6 0 0 0-.768-.827l-.009-.006a2.3 2.3 0 0 0-.776-.38l-.016-.004l-.32-.048a1.05 1.05 0 0 0-.471.074l.007-.003q-.128.072-.08.184q.058.128.145.225l-.001-.001q.092.108.205.19l.003.002l.112.08c.283.148.516.354.693.603l.004.006c.191.237.359.505.494.792l.01.024l.16.368c.135.402.38.738.7.981l.005.004c.3.234.662.402 1.057.478l.016.002c.33.064.714.104 1.106.112h.007q.069.002.15.002q.392 0 .767-.062l-.027.004l.368-.064q0 .609.008 1.418t.008.873v.014c0 .185-.08.351-.208.466h-.001a.72.72 0 0 1-.645.111l.005.001C3.486 22.286.006 17.692.006 12.285c0-2.268.612-4.393 1.681-6.219l-.032.058a12.35 12.35 0 0 1 4.422-4.449l.058-.031a11.9 11.9 0 0 1 6.073-1.645h.098h-.005zm-7.64 17.666q.048-.112-.112-.192q-.16-.048-.208.032q-.048.112.112.192q.144.096.208-.032m.497.545q.112-.08-.032-.256q-.16-.144-.256-.048q-.112.08.032.256q.159.157.256.047zm.48.72q.144-.112 0-.304q-.128-.208-.272-.096q-.144.08 0 .288t.272.112m.672.673q.128-.128-.064-.304q-.192-.192-.32-.048q-.144.128.064.304q.192.192.32.044zm.913.4q.048-.176-.208-.256q-.24-.064-.304.112t.208.24q.24.097.304-.096m1.009.08q0-.208-.272-.176q-.256 0-.256.176q0 .208.272.176q.256.001.256-.175zm.929-.16q-.032-.176-.288-.144q-.256.048-.224.24t.288.128t.225-.224z"
/>
</svg>
{:else if i === "website"}
<svg
xmlns="http://www.w3.org/2000/svg"
width={wh}
height={wh}
viewBox="0 0 2048 2048"
>
<path
fill="currentColor"
d="M1024 0q141 0 272 36t245 103t207 160t160 208t103 245t37 272q0 141-36 272t-103 245t-160 207t-208 160t-245 103t-272 37q-141 0-272-36t-245-103t-207-160t-160-208t-103-244t-37-273q0-141 36-272t103-245t160-207t208-160T751 37t273-37m0 1920q123 0 237-32t214-90t182-141t140-181t91-214t32-238q0-123-32-237t-90-214t-141-182t-181-140t-214-91t-238-32q-123 0-237 32t-214 90t-182 141t-140 181t-91 214t-32 238q0 123 32 237t90 214t141 182t181 140t214 91t238 32m597-880l48-144h75l-85 256h-75l-48-144l-48 144h-75l-85-256h75l48 144l48-144h74zm-464-144h75l-85 256h-75l-48-144l-48 144h-75l-85-256h75l48 144l48-144h74l48 144zm-512 0h75l-85 256h-75l-48-144l-48 144h-75l-85-256h75l48 144l48-144h74l48 144z"
/>
</svg>
{:else if i === "tmdb"}
<svg
xmlns="http://www.w3.org/2000/svg"
width={wh}
height={wh}
viewBox="0 0 185.04 133.4"
>
<path
fill="currentColor"
d="M51.06,66.7h0A17.67,17.67,0,0,1,68.73,49h-.1A17.67,17.67,0,0,1,86.3,66.7h0A17.67,17.67,0,0,1,68.63,84.37h.1A17.67,17.67,0,0,1,51.06,66.7Zm82.67-31.33h32.9A17.67,17.67,0,0,0,184.3,17.7h0A17.67,17.67,0,0,0,166.63,0h-32.9A17.67,17.67,0,0,0,116.06,17.7h0A17.67,17.67,0,0,0,133.73,35.37Zm-113,98h63.9A17.67,17.67,0,0,0,102.3,115.7h0A17.67,17.67,0,0,0,84.63,98H20.73A17.67,17.67,0,0,0,3.06,115.7h0A17.67,17.67,0,0,0,20.73,133.37Zm83.92-49h6.25L125.5,49h-8.35l-8.9,23.2h-.1L99.4,49H90.5Zm32.45,0h7.8V49h-7.8Zm22.2,0h24.95V77.2H167.1V70h15.35V62.8H167.1V56.2h16.25V49h-24ZM10.1,35.4h7.8V6.9H28V0H0V6.9H10.1ZM39,35.4h7.8V20.1H61.9V35.4h7.8V0H61.9V13.2H46.75V0H39Zm41.25,0h25V28.2H88V21h15.35V13.8H88V7.2h16.25V0h-24Zm-79,49H9V57.25h.1l9,27.15H24l9.3-27.15h.1V84.4h7.8V49H29.45l-8.2,23.1h-.1L13,49H1.2Zm112.09,49H126a24.59,24.59,0,0,0,7.56-1.15,19.52,19.52,0,0,0,6.35-3.37,16.37,16.37,0,0,0,4.37-5.5A16.91,16.91,0,0,0,146,115.8a18.5,18.5,0,0,0-1.68-8.25,15.1,15.1,0,0,0-4.52-5.53A18.55,18.55,0,0,0,133.07,99,33.54,33.54,0,0,0,125,98H113.29Zm7.81-28.2h4.6a17.43,17.43,0,0,1,4.67.62,11.68,11.68,0,0,1,3.88,1.88,9,9,0,0,1,2.62,3.18,9.87,9.87,0,0,1,1,4.52,11.92,11.92,0,0,1-1,5.08,8.69,8.69,0,0,1-2.67,3.34,10.87,10.87,0,0,1-4,1.83,21.57,21.57,0,0,1-5,.55H121.1Zm36.14,28.2h14.5a23.11,23.11,0,0,0,4.73-.5,13.38,13.38,0,0,0,4.27-1.65,9.42,9.42,0,0,0,3.1-3,8.52,8.52,0,0,0,1.2-4.68,9.16,9.16,0,0,0-.55-3.2,7.79,7.79,0,0,0-1.57-2.62,8.38,8.38,0,0,0-2.45-1.85,10,10,0,0,0-3.18-1v-.1a9.28,9.28,0,0,0,4.43-2.82,7.42,7.42,0,0,0,1.67-5,8.34,8.34,0,0,0-1.15-4.65,7.88,7.88,0,0,0-3-2.73,12.9,12.9,0,0,0-4.17-1.3,34.42,34.42,0,0,0-4.63-.32h-13.2Zm7.8-28.8h5.3a10.79,10.79,0,0,1,1.85.17,5.77,5.77,0,0,1,1.7.58,3.33,3.33,0,0,1,1.23,1.13,3.22,3.22,0,0,1,.47,1.82,3.63,3.63,0,0,1-.42,1.8,3.34,3.34,0,0,1-1.13,1.2,4.78,4.78,0,0,1-1.57.65,8.16,8.16,0,0,1-1.78.2H165Zm0,14.15h5.9a15.12,15.12,0,0,1,2.05.15,7.83,7.83,0,0,1,2,.55,4,4,0,0,1,1.58,1.17,3.13,3.13,0,0,1,.62,2,3.71,3.71,0,0,1-.47,1.95,4,4,0,0,1-1.23,1.3,4.78,4.78,0,0,1-1.67.7,8.91,8.91,0,0,1-1.83.2h-7Z"
/>
</svg>
{:else if i === "igdb"}
<svg
xmlns="http://www.w3.org/2000/svg"
width={wh}
height={wh}
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M24 6.228H0v11.543a89 89 0 0 1 2.271-.333a74 74 0 0 1 17.038-.28c1.57.153 3.134.363 4.69.614zm-.706.707v10.013a74.8 74.8 0 0 0-22.588 0V6.934zM7.729 8.84a2.62 2.62 0 0 0-1.857.72a2.55 2.55 0 0 0-.73 1.33c-.098.5-.063 1.03.112 1.51c.177.488.515.917.954 1.196c.547.354 1.224.472 1.865.401a3.24 3.24 0 0 0 1.786-.777c-.003-.724.002-1.449-.002-2.173c-.725.004-1.45-.002-2.174.003c.003.317 0 .634.001.951h1.105c.002.236 0 .473.002.71a1.7 1.7 0 0 1-.932.298c-.32.02-.65-.05-.922-.225a1.46 1.46 0 0 1-.59-.744c-.18-.499-.134-1.085.163-1.53c.23-.355.619-.61 1.043-.647a1.8 1.8 0 0 1 1.012.206c.152.082.286.192.424.295q.344-.42.692-.838a3 3 0 0 0-.595-.403c-.418-.212-.892-.285-1.357-.283m11.66.086q-.14.002-.28 0c-.68.002-1.359-.004-2.038.003c.003 1.666 0 3.332.002 4.998h2.497q.36-.003.709-.097c.276-.076.546-.208.742-.422c.194-.208.297-.492.304-.776c.016-.278-.032-.572-.195-.804c-.175-.252-.453-.408-.734-.514c.211-.122.407-.285.521-.505c.134-.246.149-.535.117-.807a1.16 1.16 0 0 0-.436-.73c-.264-.207-.599-.304-.93-.334a3 3 0 0 0-.279-.012m-16.715 0v5.002h1.102V8.927q-.552-.002-1.102 0zm8.524 0v5.002h2.016a2.9 2.9 0 0 0 1.07-.211a2.44 2.44 0 0 0 1.174-.993c.34-.555.429-1.244.292-1.876a2.37 2.37 0 0 0-.828-1.338c-.478-.387-1.096-.577-1.707-.584zm6.949.967c.392.002.784-.001 1.176.002c.183.011.38.054.51.19c.11.112.136.28.112.43a.44.44 0 0 1-.22.316a1.1 1.1 0 0 1-.483.116c-.365.002-.73-.001-1.094.001l-.001-1.054zm-5.031.026c.28 0 .567.053.815.19c.274.149.491.396.607.685c.113.272.138.574.107.865a1.46 1.46 0 0 1-.335.786a1.43 1.43 0 0 1-.865.466c-.168.031-.34.022-.51.023h-.632V9.92zm5.03 1.948h1.36c.174.006.354.035.505.127a.45.45 0 0 1 .212.308c.025.15.004.32-.099.44c-.102.12-.258.176-.409.2c-.172.032-.348.02-.522.022c-.35-.001-.698.002-1.047-.001z"
/>
</svg>
{/if}
<style lang="scss">
+22 -1
View File
@@ -100,10 +100,31 @@
}
}
a.menu-footer,
button.menu-footer {
font-size: 11px;
padding: 2px;
text-align: center;
transition: inherit;
color: $text-color-accent;
cursor: pointer;
border: none;
display: inline;
font-weight: initial;
width: auto;
&:hover,
&:focus-visible {
text-decoration: underline;
background-color: $bg-color;
color: $text-color;
}
}
span {
margin-top: 8px;
font-size: 11px;
color: gray;
color: $text-color-accent;
text-align: center;
}
}
+2 -1
View File
@@ -1,5 +1,6 @@
<script lang="ts">
import type { PublicUser } from "@/types";
import { toBaseUrl } from "./util/url";
interface Props {
users: PublicUser[];
@@ -13,7 +14,7 @@
<ul>
{#each users as user}
<li title={user.username}>
<a href="/lists/{user.id}/{user.username}">
<a href={toBaseUrl(`/lists/${user.id}/${user.username}`)}>
<span>{user.username}</span>
</a>
</li>
+2 -1
View File
@@ -9,6 +9,7 @@
import { getLatestWatchedInTv } from "./util/helpers";
import { notify } from "./util/notify";
import { untrack } from "svelte";
import { toBaseUrl } from "./util/url";
interface Props {
list: Watched[];
@@ -296,7 +297,7 @@
<h4 class="norm">
Try searching for something you would like to add.
</h4>
<button onclick={() => goto("/import")}>Import</button>
<button onclick={() => goto(toBaseUrl("/import"))}>Import</button>
{/if}
{/if}
</div>
+2 -1
View File
@@ -6,6 +6,7 @@
import Spinner from "../Spinner.svelte";
import { clearWatcharrData } from ".";
import { goto } from "$app/navigation";
import { toBaseUrl } from "../util/url";
interface Props {
onClose: () => void;
@@ -34,7 +35,7 @@
function logout() {
clearWatcharrData();
goto("/login?noAuto=1");
goto(toBaseUrl("/login?noAuto=1"));
}
function proxyLogout() {
+101
View File
@@ -0,0 +1,101 @@
<script lang="ts">
import Icon from "../Icon.svelte";
import Modal from "../Modal.svelte";
interface Props {
onClose: () => void;
}
let { onClose }: Props = $props();
</script>
<Modal title="About" {onClose} maxWidth="400px">
<div class="content">
<div class="inner">
<p>
An open source project that helps you keep the content you are watching
or playing organized and places you in control.
</p>
<div class="horizontal-icon-list">
<a
href="https://github.com/sbondCo/Watcharr"
target="_blank"
title="Github"
>
<Icon i="github" wh={60} />
Source<br />
Code
</a>
<a
href="https://watcharr.app/"
target="_blank"
title="Watcharr Website & Documentation"
>
<Icon i="website" wh={60} />
Website<br />
& Docs
</a>
</div>
<h5 class="norm">Watcharr uses the following media databases:</h5>
<div class="horizontal-icon-list">
<a href="https://www.themoviedb.org/" target="_blank" title="TMDB">
<Icon i="tmdb" wh={60} />
</a>
<a href="https://www.igdb.com/" target="_blank" title="IGDB">
<Icon i="igdb" wh={60} />
</a>
</div>
<h5 class="norm">
<!-- Linking to the file in `dev` branch would break every old version of
Watcharr if the file were ever moved in the future. Using the current version
as the branch should ensure this file is always accessible (but only as up to
date as the current version) -->
<a
href="https://github.com/sbondCo/Watcharr/blob/v{__WATCHARR_VERSION__}/ATTRIBUTION.md"
target="_blank"
>
See our attribution file.
</a>
</h5>
</div>
</div>
</Modal>
<style lang="scss">
p {
margin: 8px 0px;
}
h5.norm {
margin: 8px 0px;
> a {
text-decoration: underline;
}
}
div.horizontal-icon-list {
display: flex;
justify-content: center;
align-items: center;
padding-bottom: 20px;
padding-top: 5px;
> a {
display: flex;
flex-flow: column;
align-items: center;
gap: 5px;
margin: 0 10px;
text-align: center;
}
}
a {
transition: opacity 150ms ease-in;
&:hover {
opacity: 0.5;
}
}
</style>
+37 -7
View File
@@ -7,9 +7,12 @@
import { goto } from "$app/navigation";
import { clearWatcharrData } from "../logout";
import { notify } from "../util/notify";
import AboutModal from "./AboutModal.svelte";
import { toBaseUrl } from "../util/url";
let user = $derived(store.userInfo);
let proxyUserLogoutShown = $state(false);
let aboutModalOpen = $state(false);
function logout() {
if (user?.type === UserType.Proxy) {
@@ -18,23 +21,23 @@
return;
}
clearWatcharrData();
goto("/login");
goto(toBaseUrl("/login"));
}
function profile() {
goto("/profile");
goto(toBaseUrl("/profile"));
}
function serverSettings() {
goto("/server");
goto(toBaseUrl("/server"));
}
function userManagement() {
goto("/manage_users");
goto(toBaseUrl("/manage_users"));
}
function requestManagement() {
goto("/arr_requests");
goto(toBaseUrl("/arr_requests"));
}
function shareWatchedList() {
@@ -42,7 +45,9 @@
const ud = parseTokenPayload();
console.log(ud);
if (ud?.userId && ud?.username) {
const shareLink = `${window.location.origin}/lists/${ud.userId}/${ud.username}`;
const shareLink = `${window.location.origin}${toBaseUrl(
`/lists/${ud.userId}/${ud.username}`,
)}`;
navigator.clipboard
.writeText(shareLink)
.then(() => {
@@ -61,6 +66,10 @@
notify({ id: nid, type: "error", text: "Failed to get link" });
}
}
function closeAbout() {
aboutModalOpen = false;
}
</script>
<Menu conf={{ arrowRight: "10px" }}>
@@ -85,5 +94,26 @@
{#if proxyUserLogoutShown}
<ProxyUserLogoutModal onClose={() => (proxyUserLogoutShown = false)} />
{/if}
<span>v{__WATCHARR_VERSION__}</span>
<span>
<button
class="menu-footer"
onclick={() => {
aboutModalOpen = !aboutModalOpen;
}}
>
about
</button>
|
<a
class="menu-footer"
href="https://github.com/sbondCo/Watcharr/releases"
target="_blank"
>
v{__WATCHARR_VERSION__}
</a>
</span>
</Menu>
{#if aboutModalOpen}
<AboutModal onClose={closeAbout} />
{/if}
+4 -1
View File
@@ -1,6 +1,7 @@
<script lang="ts">
import { store } from "@/store.svelte";
import Menu from "../Menu.svelte";
import { toBaseUrl } from "../util/url";
interface Props {
close: () => {};
@@ -15,7 +16,9 @@
<div class="list">
{#each store.follows as f}
<a
href="/lists/{f.followedUser.id}/{f.followedUser.username}"
href={toBaseUrl(
`/lists/${f.followedUser.id}/${f.followedUser.username}`,
)}
onclick={() => close()}
>
{f.followedUser.username}
+2 -1
View File
@@ -18,6 +18,7 @@
import PosterRating from "./PosterRating.svelte";
import { decode } from "blurhash";
import ExtraDetails from "./ExtraDetails.svelte";
import { toBaseUrl } from "../util/url";
interface Props {
id?: number | undefined; // Watched list id
@@ -72,7 +73,7 @@
const poster = media.poster?.path
? `${baseURL}/${media.poster.path}`
: `https://images.igdb.com/igdb/image/upload/t_cover_big/${media.coverId}.jpg`;
const link = media.id ? `/game/${media.id}` : undefined;
const link = media.id ? toBaseUrl(`/game/${media.id}`) : undefined;
const dateStr = media.firstReleaseDate;
const year = dateStr ? new Date(dateStr).getFullYear() : undefined;
+2 -1
View File
@@ -4,6 +4,7 @@
addClassToParent,
calculateTransformOrigin,
} from "@/lib/util/helpers";
import { toBaseUrl } from "../util/url";
interface Props {
id: number | undefined;
@@ -24,7 +25,7 @@
const poster = path
? `https://image.tmdb.org/t/p/w300_and_h450_bestv2${path}`
: undefined;
const link = id ? `/person/${id}` : undefined;
const link = id ? toBaseUrl(`/person/${id}`) : undefined;
</script>
<!-- Quick fix to ignore error, should be fixed -->
+2 -1
View File
@@ -13,6 +13,7 @@
import PosterStatus from "./PosterStatus.svelte";
import PosterRating from "./PosterRating.svelte";
import ExtraDetails from "./ExtraDetails.svelte";
import { toBaseUrl } from "../util/url";
interface Props {
id?: number | undefined; // Watched list id
@@ -84,7 +85,7 @@
: `https://image.tmdb.org/t/p/w500${media.poster_path}`,
);
let link = $derived(
media.id ? `/${media.media_type}/${media.id}` : undefined,
media.id ? toBaseUrl(`/${media.media_type}/${media.id}`) : undefined,
);
let dateStr = $derived(media.release_date || media.first_air_date);
let year = $derived(dateStr ? new Date(dateStr).getFullYear() : undefined);
+2 -1
View File
@@ -16,6 +16,7 @@ import {
import axios from "axios";
import { notify, unNotify } from "./notify";
import { browser } from "$app/environment";
import { toBaseUrl } from "./url";
const { MODE } = import.meta.env;
export const baseURL =
@@ -23,7 +24,7 @@ export const baseURL =
? browser
? `${location.protocol}//${location.hostname}:3080/api`
: "http://127.0.0.1:3080/api"
: "/api";
: toBaseUrl("/api");
console.log("api: baseURL constructed:", baseURL);
/**
+10
View File
@@ -0,0 +1,10 @@
import { base } from "$app/paths";
/**
* Takes in a `path` and prefixes it with `base`.
* This enables support for using base paths to access
* the frontend by fixing all absolute paths passed through.
*/
export function toBaseUrl(path: string) {
return base + path;
}
+8 -6
View File
@@ -1,5 +1,6 @@
<script lang="ts">
import { afterNavigate, goto } from "$app/navigation";
import { base } from "$app/paths";
import { page } from "$app/state";
import Icon from "@/lib/Icon.svelte";
import PageError from "@/lib/PageError.svelte";
@@ -12,6 +13,7 @@
import SortMenu from "@/lib/nav/SortMenu.svelte";
import TagMenu from "@/lib/tag/TagMenu.svelte";
import { isTouch } from "@/lib/util/helpers";
import { toBaseUrl } from "@/lib/util/url";
import { store, defaultSort } from "@/store.svelte";
import axios from "axios";
import { onMount } from "svelte";
@@ -34,7 +36,7 @@
function handleProfileClick() {
if (!localStorage.getItem("token")) {
goto("/login");
goto(toBaseUrl("/login"));
} else {
closeAllSubMenus("sub");
subMenuShown = !subMenuShown;
@@ -64,7 +66,7 @@
// Using autofocus seems to work. Disables after goto runs.
// https://github.com/sbondCo/Watcharr/issues/169
target.autofocus = true;
goto(`/search?q=${encodeURIComponent(query)}`).then(() => {
goto(toBaseUrl(`/search?q=${encodeURIComponent(query)}`)).then(() => {
// Use mainSearchEl if nav not split, otherwise use ev target.
if (
!document.body.classList.contains("split-nav") &&
@@ -112,7 +114,7 @@
store.tags = ts.data;
}
} else {
goto("/login?again=1");
goto(toBaseUrl("/login?again=1"));
}
}
@@ -187,7 +189,7 @@
<nav bind:this={navEl}>
<div class="wrapper">
<a href="/">
<a href={toBaseUrl("/")}>
<span class="large">Watcharr</span>
<span class="small">W</span>
</a>
@@ -278,7 +280,7 @@
{#if tagMenuShown}
<TagMenu
onTagClick={(tag) => {
goto(`/tag/${tag.id}`);
goto(toBaseUrl(`/tag/${tag.id}`));
tagMenuShown = false;
}}
showManageBtn={true}
@@ -286,7 +288,7 @@
{/if}
<button
class="plain other discover"
onclick={() => goto("/discover")}
onclick={() => goto(toBaseUrl("/discover"))}
use:tooltip={{ text: "Discover", pos: "bot" }}
>
<Icon i="compass" wh={26} />
+3 -2
View File
@@ -7,6 +7,7 @@ import axios from "axios";
import { baseURL } from "@/lib/util/api";
import { notify } from "@/lib/util/notify";
import { clearWatcharrData } from "@/lib/logout";
import { toBaseUrl } from "@/lib/util/url";
axios.interceptors.request.use(
(config) => {
@@ -18,7 +19,7 @@ axios.interceptors.request.use(
// Don't require token check if going to auth route (login/register)
if (!token && !config.url?.includes("/auth")) {
console.error("No token, going to login. Endpoint:", config.url);
goto("/login?again=1");
goto(toBaseUrl("/login?again=1"));
throw new axios.Cancel("No auth token found");
}
config.headers.set("Authorization", token);
@@ -40,7 +41,7 @@ axios.interceptors.response.use(
console.error("Recieved 401 response, going to login.");
notify({ text: "Request Authorization Failed!", type: "error" });
clearWatcharrData();
goto("/login?again=1");
goto(toBaseUrl("/login?again=1"));
}
return Promise.reject(error);
},
+2 -1
View File
@@ -6,6 +6,7 @@
import { baseURL } from "@/lib/util/api";
import { toRelativeDate } from "@/lib/util/helpers";
import { notify } from "@/lib/util/notify";
import { toBaseUrl } from "@/lib/util/url";
import {
type ArrRequestResponse,
type TMDBMovieDetails,
@@ -94,7 +95,7 @@
<h2 class="norm">
<a
data-sveltekit-preload-data="tap"
href={`/${r.content.type}/${r.content.tmdbId}`}
href={toBaseUrl(`/${r.content.type}/${r.content.tmdbId}`)}
class="plain"
>
{r.content.title}
+9 -8
View File
@@ -26,6 +26,7 @@
TodoMoviesMovie,
} from "@/types";
import Icon from "@/lib/Icon.svelte";
import { toBaseUrl } from "@/lib/util/url";
let isDragOver = $state(false);
let isLoading = $state(false);
@@ -83,7 +84,7 @@
data: r.result.toString(),
type,
};
goto("/import/process");
goto(toBaseUrl("/import/process"));
}
},
false,
@@ -255,7 +256,7 @@
data: JSON.stringify(toImport),
type: "movary",
};
goto("/import/process");
goto(toBaseUrl("/import/process"));
} catch (err) {
isLoading = false;
notify({ type: "error", text: "Failed to read files!" });
@@ -331,7 +332,7 @@
data: JSON.stringify(toImport),
type: "watcharr",
};
goto("/import/process");
goto(toBaseUrl("/import/process"));
} catch (err) {
isLoading = false;
notify({ type: "error", text: "Failed to read file!" });
@@ -380,7 +381,7 @@
data: r.result.toString(),
type: "myanimelist",
};
goto("/import/process");
goto(toBaseUrl("/import/process"));
}
},
false,
@@ -539,7 +540,7 @@
data: JSON.stringify(toImport),
type: "ryot",
};
goto("/import/process");
goto(toBaseUrl("/import/process"));
} catch (err) {
isLoading = false;
notify({ type: "error", text: "Failed to read file!" });
@@ -657,7 +658,7 @@
type: "todomovies",
};
goto("/import/process");
goto(toBaseUrl("/import/process"));
} catch (err) {
isLoading = false;
notify({ type: "error", text: "Failed to read files!" });
@@ -667,7 +668,7 @@
onMount(() => {
if (!localStorage.getItem("token")) {
goto("/login");
goto(toBaseUrl("/login"));
}
});
</script>
@@ -702,7 +703,7 @@
filesSelected={(f) => processFiles(f, "tmdb")}
/>
<button class="plain" onclick={() => goto("/import/trakt")}>
<button class="plain" onclick={() => goto(toBaseUrl("/import/trakt"))}>
<Icon i="trakt" wh="100%" />
<h4 class="norm">Trakt Import</h4>
</button>
+6 -4
View File
@@ -29,6 +29,7 @@
import { onDestroy } from "svelte";
import papa from "papaparse";
import Status from "@/lib/Status.svelte";
import { toBaseUrl } from "@/lib/util/url";
interface ImportedListItemMultiProblem {
original: ImportedList;
@@ -58,7 +59,7 @@
const list = store.importedList;
if (!list) {
console.log("import/process, no list, returning to /import");
goto("/import");
goto(toBaseUrl("/import"));
return;
}
console.log("getList", list);
@@ -431,14 +432,14 @@
) {
// Some items failed.. go to some-failed
store.parsedImportedList = rList;
goto("/import/some-failed");
goto(toBaseUrl("/import/some-failed"));
} else {
notify({
type: "success",
text: "All content successfully imported! Try refreshing if you are missing data.",
time: 15000,
});
goto("/");
goto(toBaseUrl("/"));
}
}
@@ -677,7 +678,8 @@
</tbody>
</table>
<div class="btns">
<button onclick={() => goto("/import")}><Icon i="arrow" />Back</button
<button onclick={() => goto(toBaseUrl("/import"))}
><Icon i="arrow" />Back</button
>
<button onclick={() => changeAllStatuses()} disabled={isImporting}>
Change Statuses
@@ -12,6 +12,7 @@
<script lang="ts">
import { goto } from "$app/navigation";
import { toBaseUrl } from "@/lib/util/url";
import { store } from "@/store.svelte";
import { ImportResponseType, type ImportedList } from "@/types";
import { onMount } from "svelte";
@@ -36,7 +37,7 @@
}
console.log("failedlen", failed.length);
} else {
goto("/import");
goto(toBaseUrl("/import"));
}
});
</script>
+2 -1
View File
@@ -21,6 +21,7 @@
import SyncModal from "./modals/SyncModal.svelte";
import RegionDropDown from "@/lib/RegionDropDown.svelte";
import RatingSetting from "@/lib/rating/RatingSetting.svelte";
import { toBaseUrl } from "@/lib/util/url";
let user = $derived(store.userInfo);
let settings = $derived(store.userSettings);
@@ -345,7 +346,7 @@
<RatingSetting />
<div class="row btns">
<button onclick={() => goto("/import")}>Import</button>
<button onclick={() => goto(toBaseUrl("/import"))}>Import</button>
<button onclick={() => downloadWatchedList()} disabled={exportDisabled}
>Export</button
>
+2 -1
View File
@@ -1,6 +1,7 @@
<script lang="ts">
import { goto } from "$app/navigation";
import { notify } from "@/lib/util/notify";
import { toBaseUrl } from "@/lib/util/url";
import { store } from "@/store.svelte";
import { UserPermission } from "@/types";
import axios from "axios";
@@ -32,7 +33,7 @@
if (store.userInfo) {
store.userInfo.permissions = UserPermission.PERM_ADMIN;
}
goto("/");
goto(toBaseUrl("/"));
})
.catch((err) => {
console.error(
+8 -3
View File
@@ -29,6 +29,7 @@
import Icon from "@/lib/Icon.svelte";
import { afterNavigate, goto } from "$app/navigation";
import { page } from "$app/state";
import { toBaseUrl } from "@/lib/util/url.js";
type GameWithMediaType = GameSearch & { media_type: "game" };
type CombinedResult =
@@ -278,7 +279,7 @@
data[0],
);
goto(`/game/${data[0].id}`);
goto(toBaseUrl(`/game/${data[0].id}`));
return;
}
allSearchResults.push(...data);
@@ -290,7 +291,7 @@
// assuming that people paste the id in, this should work
// without the debounce going to an incomplete id.
// Flesh out if anyone has issues.
goto(`/${extProvider.provider}/${extProvider.id}`);
goto(toBaseUrl(`/${extProvider.provider}/${extProvider.id}`));
return;
} else {
// Else call tmdb `external id` endpoint
@@ -324,7 +325,11 @@
mediaType,
);
} else {
goto(`/${data.results[0].media_type}/${data.results[0].id}`);
goto(
toBaseUrl(
`/${data.results[0].media_type}/${data.results[0].id}`,
),
);
return;
}
}
+9 -3
View File
@@ -24,6 +24,7 @@
import RegionDropDown from "@/lib/RegionDropDown.svelte";
import TaskScheduleModal from "./modals/TaskScheduleModal.svelte";
import TrustedHeaderAuthModal from "./modals/TrustedHeaderAuthModal.svelte";
import { toBaseUrl } from "@/lib/util/url";
let serverConfig: ServerConfig | undefined = $state();
let jellyfinOrEmby = $derived(serverConfig?.USE_EMBY ? "Emby" : "Jellyfin");
@@ -108,7 +109,12 @@
{#await getServerStats()}
<Spinner />
{:then stats}
<Stat name="Users" value={stats.users} href="/manage_users" large />
<Stat
name="Users"
value={stats.users}
href={toBaseUrl("/manage_users")}
large
/>
<Stat name="Private Users" value={stats.privateUsers} large />
<Stat name="Watched Movies" value={stats.watchedMovies} large />
<Stat name="Watched Shows" value={stats.watchedShows} large />
@@ -118,14 +124,14 @@
<Stat
name="Most Watched Movie"
value={stats.mostWatchedMovie.title}
href="/movie/{stats.mostWatchedMovie.tmdbId}"
href={toBaseUrl(`/movie/${stats.mostWatchedMovie.tmdbId}`)}
/>
{/if}
{#if stats.mostWatchedShow?.title}
<Stat
name="Most Watched Show"
value={stats.mostWatchedShow.title}
href="/tv/{stats.mostWatchedShow.tmdbId}"
href={toBaseUrl(`/tv/${stats.mostWatchedShow.tmdbId}`)}
/>
{/if}
{:catch err}
+6 -5
View File
@@ -6,6 +6,7 @@
import { noAuthAxios } from "@/lib/util/api";
import { onMount } from "svelte";
import { notify, unNotify } from "@/lib/util/notify";
import { toBaseUrl } from "@/lib/util/url";
let error: string | undefined = $state();
let login = $state(true);
@@ -18,7 +19,7 @@
onMount(() => {
if (localStorage.getItem("token")) {
goto("/");
goto(toBaseUrl("/"));
}
if (!error && page.url.searchParams.get("again")) {
@@ -35,7 +36,7 @@
if (r?.data) {
if (r.data.isInSetup) {
console.log("Server is in setup.. navigating to web setup page.");
goto("/setup");
goto(toBaseUrl("/setup"));
}
availableProviders = r.data.available;
apHeader = availableProviders?.includes("header");
@@ -83,7 +84,7 @@
} else {
localStorage.removeItem("useEmby");
}
goto("/");
goto(toBaseUrl("/"));
notify({ id: nid, text: `Welcome ${user}!`, type: "success" });
}
})
@@ -120,7 +121,7 @@
if (resp.data?.token) {
console.log("Received token... logging in.");
localStorage.setItem("token", resp.data.token);
goto("/");
goto(toBaseUrl("/"));
notify({ id: nid, text: `Welcome!`, type: "success" });
}
})
@@ -148,7 +149,7 @@
if (resp.data?.token) {
console.log("Received token... logging in.");
localStorage.setItem("token", resp.data.token);
goto("/");
goto(toBaseUrl("/"));
notify({ id: nid, text: `Welcome!`, type: "success" });
}
})
+7 -7
View File
@@ -1,30 +1,30 @@
<script lang="ts">
import { preventDefault } from "svelte/legacy";
import { goto } from "$app/navigation";
import type { AvailableAuthProviders } from "@/types";
import { noAuthAxios } from "@/lib/util/api";
import { onMount } from "svelte";
import { notify, unNotify } from "@/lib/util/notify";
import { toBaseUrl } from "@/lib/util/url";
let error: string = $state();
let error: string | undefined = $state();
onMount(() => {
if (localStorage.getItem("token")) {
goto("/");
goto(toBaseUrl("/"));
}
noAuthAxios.get<AvailableAuthProviders>("/auth/available").then((r) => {
if (r?.data) {
if (!r?.data?.isInSetup) {
console.log("Server not in setup.. navigating to login page.");
goto("/login");
goto(toBaseUrl("/login"));
}
}
});
});
function handleLogin(ev: SubmitEvent) {
ev.preventDefault();
const fd = new FormData(ev.target! as HTMLFormElement);
const user = fd.get("username");
const pass = fd.get("password");
@@ -44,7 +44,7 @@
if (resp.data?.token) {
console.log("Received token... logging in.");
localStorage.setItem("token", resp.data.token);
goto("/");
goto(toBaseUrl("/"));
notify({ id: nid, text: `Welcome ${user}!`, type: "success" });
}
})
@@ -74,7 +74,7 @@
<span class="error">{error}!</span>
{/if}
<form onsubmit={preventDefault(handleLogin)}>
<form onsubmit={handleLogin}>
<label for="username">Username</label>
<input type="text" name="username" placeholder="Username" />
+2 -1
View File
@@ -1,6 +1,7 @@
<script>
import { goto } from "$app/navigation";
import { page } from "$app/state";
import { toBaseUrl } from "@/lib/util/url";
</script>
<div>
@@ -16,7 +17,7 @@
<h4 class="norm">We couldn't load this page</h4>
{/if}
<div class="btns">
<button onclick={() => goto("/")}>Home</button>
<button onclick={() => goto(toBaseUrl("/"))}>Home</button>
<button onclick={() => location.reload()}>Refresh</button>
</div>
</div>
+5 -1
View File
@@ -57,7 +57,11 @@ export type Icon =
| "sparkles"
| "tag"
| "ticket"
| "lock-closed";
| "lock-closed"
| "github"
| "website"
| "tmdb"
| "igdb";
export type Theme = "light" | "dark";
+13
View File
@@ -2,6 +2,14 @@ import adapter from "@sveltejs/adapter-node";
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
import { sveltePreprocess } from "svelte-preprocess";
if (
process.env.WATCHARR_BASE &&
(!process.env.WATCHARR_BASE.startsWith("/") ||
process.env.WATCHARR_BASE.endsWith("/"))
) {
throw new Error("WATCHARR_BASE must start with, but not end with '/'");
}
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
@@ -21,6 +29,11 @@ const config = {
alias: {
"@": "src",
},
paths: {
base: process.env.WATCHARR_BASE,
relative: true,
},
},
};