diff --git a/.dockerignore b/.dockerignore index 1f9515c..8708dce 100644 --- a/.dockerignore +++ b/.dockerignore @@ -5,7 +5,6 @@ # Only add necessary files to the Docker build context (Dockerfiles are always included implicitly) !/build/ !/internal/ -!/pkg/ !/go.mod !/go.sum !main.go diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml deleted file mode 100644 index bdd4fe6..0000000 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: Bug report -description: Let us know if something isn't working as expected -labels: ["bug report"] -body: - - type: markdown - attributes: - value: | - > [!NOTE] - > - > Do not prefix your title with "[BUG]", "[Bug report]", etc., a label will be added automatically. - - If you're unsure whether you're experiencing a bug or not, consider using the [Discussions](https://github.com/glanceapp/glance/discussions) or [Discord](https://discord.com/invite/7KQ7Xa9kJd) to ask for help. - - Please include only the information you think is relevant to the bug: - - * How did you install Glance? (Docker container, manual binary install, etc) - * Which version of Glance are you using? - * Include the relevant parts of your `glance.yml` if applicable (widget, data source, properties used, etc) - * Include any relevant logs or screenshots if applicable - * Is the issue specific to a certain browser or OS? - * Steps to reliably reproduce the issue - * Are you hosting Glance on a VPS? - * Anything else you think might be relevant - - **No need to copy the above list into your description, it's just a guide to help you provide the most useful information.** - - - type: textarea - id: description - validations: - required: true - attributes: - label: Description - - - type: markdown - attributes: - value: | - Thank you for taking the time to submit a bug report. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml deleted file mode 100644 index e8c34af..0000000 --- a/.github/ISSUE_TEMPLATE/config.yml +++ /dev/null @@ -1,8 +0,0 @@ -blank_issues_enabled: false -contact_links: - - name: Discussions - url: https://github.com/glanceapp/glance/discussions - about: For help, feedback, guides, resources and more - - name: Discord - url: https://discord.com/invite/7KQ7Xa9kJd - about: Much like the discussions but more chatty diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml deleted file mode 100644 index d8f5343..0000000 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: Feature request -description: Share your ideas for new features or improvements -labels: ["feature request"] -body: - - type: markdown - attributes: - value: | - > [!NOTE] - > - > Do not prefix your title with "[REQUEST]", "[Feature request]", etc., a label will be added automatically. - - Please provide a detailed description of what the feature would do and what it would look like: - - * What problem would this feature solve? - * Are there any potential downsides to this feature? - * If applicable, what would the configuration for this feature look like? - * Are there any existing examples of this feature in other software? - * If applicable, include any external documentation required to implement this feature - * Anything else you think might be relevant - - **No need to copy the above list into your description, it's just a guide to help you provide the most useful information.** - - - type: textarea - id: description - validations: - required: true - attributes: - label: Description - - - type: markdown - attributes: - value: | - Thank you for taking the time to submit your idea. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 690586f..22b3d05 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1 +1,7 @@ - + diff --git a/.gitignore b/.gitignore index 2cd84fc..f7e0f6c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ /assets /build /playground -/.idea -/glance*.yml +glance*.yml diff --git a/Dockerfile b/Dockerfile index 0c4cc63..e4019ba 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,16 +1,13 @@ -FROM golang:1.24.2-alpine3.21 AS builder +FROM golang:1.22.5-alpine3.20 AS builder WORKDIR /app COPY . /app RUN CGO_ENABLED=0 go build . -FROM alpine:3.21 +FROM alpine:3.20 WORKDIR /app COPY --from=builder /app/glance . -HEALTHCHECK --timeout=10s --start-period=60s --interval=60s \ - CMD wget --spider -q http://localhost:8080/api/healthz - EXPOSE 8080/tcp -ENTRYPOINT ["/app/glance", "--config", "/app/config/glance.yml"] +ENTRYPOINT ["/app/glance"] diff --git a/Dockerfile.goreleaser b/Dockerfile.goreleaser index bbfa8ad..dec9ac4 100644 --- a/Dockerfile.goreleaser +++ b/Dockerfile.goreleaser @@ -1,10 +1,8 @@ -FROM alpine:3.21 +FROM alpine:3.20 WORKDIR /app COPY glance . -HEALTHCHECK --timeout=10s --start-period=60s --interval=60s \ - CMD wget --spider -q http://localhost:8080/api/healthz - EXPOSE 8080/tcp -ENTRYPOINT ["/app/glance", "--config", "/app/config/glance.yml"] + +ENTRYPOINT ["/app/glance"] diff --git a/README.md b/README.md index ca16c49..0e8cfb4 100644 --- a/README.md +++ b/README.md @@ -1,436 +1,111 @@
What if you could see everything at a...
Install • Configuration • Discord • Sponsor
-Community widgets • Preconfigured pages • Themes
+Install • Configuration • Preconfigured pages • Themes • Discord
- + -## Features -### Various widgets +### Features +#### Various widgets * RSS feeds * Subreddit posts -* Hacker News posts -* Weather forecasts -* YouTube channel uploads -* Twitch channels -* Market prices -* Docker containers status -* Server stats -* Custom widgets -* [and many more...](docs/configuration.md) +* Weather +* Bookmarks +* Hacker News +* Lobsters +* Latest YouTube videos from specific channels +* Clock +* Calendar +* Stocks +* iframe +* Twitch channels & top games +* GitHub releases +* Repository overview +* Site monitor +* Search box -### Fast and lightweight -* Low memory usage -* Few dependencies -* Minimal vanilla JS -* Single <20mb binary available for multiple OSs & architectures and just as small Docker container -* Uncached pages usually load within ~1s (depending on internet speed and number of widgets) +#### Themeable + -### Tons of customizability -* Different layouts -* As many pages/tabs as you need -* Numerous configuration options for each widget -* Multiple styles for some widgets -* Custom CSS +#### Optimized for mobile devices + -### Optimized for mobile devices -Because you'll want to take it with you on the go. +#### Fast and lightweight +* Minimal JS, no bloated frameworks +* Very few dependencies +* Single, easily distributed <15mb binary and just as small docker container +* All requests are parallelized, uncached pages usually load within ~1s (depending on internet speed and number of widgets) - +### Configuration +Checkout the [configuration docs](docs/configuration.md) to learn more. A [preconfigured page](docs/configuration.md#preconfigured-page) is also available to get you started quickly. -### Themeable -Easily create your own theme by tweaking a few numbers or choose from one of the [already available themes](docs/themes.md). +### Installation +> [!CAUTION] +> +> The project is under active development, expect things to break every once in a while. - - -glance.yml
glance.yml
glance.yml
glance.yml
glance.yml
{{ .JSON.String "text" }}
-``` -glance.yml
glance.yml
{{ .JSON.String "text" }}
-{{ (.Subrequest "another-one").JSON.String "text" }}
-``` - -The subrequests support all the same properties as the main request, except for `subrequests` itself, so you can use `headers`, `parameters`, etc. - -`(.Subrequest "key")` can be a little cumbersome to write, so you can define a variable to make it easier: - -```yaml - template: | - {{ $anotherOne := .Subrequest "another-one" }} -{{ $anotherOne.JSON.String "text" }}
-``` - -You can also access the `.Response` property of a subrequest as you would with the main request: - -```yaml - template: | - {{ $anotherOne := .Subrequest "another-one" }} -{{ $anotherOne.Response.StatusCode }}
-``` - -> [!NOTE] -> -> Setting this property will override any query parameters that are already in the URL. - -```yaml -parameters: - param1: value1 - param2: - - item1 - - item2 -``` - ### Extension Display a widget provided by an external source (3rd party). If you want to learn more about developing extensions, checkout the [extensions documentation](extensions.md) (WIP). @@ -1415,24 +941,11 @@ Display a widget provided by an external source (3rd party). If you want to lear | Name | Type | Required | Default | | ---- | ---- | -------- | ------- | | url | string | yes | | -| fallback-content-type | string | no | | | allow-potentially-dangerous-html | boolean | no | false | -| headers | key & value | no | | | parameters | key & value | no | | ##### `url` -The URL of the extension. **Note that the query gets stripped from this URL and the one defined by `parameters` gets used instead.** - -##### `fallback-content-type` -Optionally specify the fallback content type of the extension if the URL does not return a valid `Widget-Content-Type` header. Currently the only supported value for this property is `html`. - -##### `headers` -Optionally specify the headers that will be sent with the request. Example: - -```yaml -headers: - x-api-key: ${SECRET_KEY} -``` +The URL of the extension. ##### `allow-potentially-dangerous-html` Whether to allow the extension to display HTML. @@ -1545,19 +1058,11 @@ You can hover over the "ERROR" text to view more information. | Name | Type | Required | Default | | ---- | ---- | -------- | ------- | | sites | array | yes | | -| style | string | no | | | show-failing-only | boolean | no | false | ##### `show-failing-only` Shows only a list of failing sites when set to `true`. -##### `style` -Used to change the appearance of the widget. Possible values are `compact`. - -Preview of `compact`: - - - ##### `sites` Properties for each site: @@ -1567,11 +1072,9 @@ Properties for each site: | title | string | yes | | | url | string | yes | | | check-url | string | no | | -| error-url | string | no | | | icon | string | no | | | allow-insecure | boolean | no | false | | same-tab | boolean | no | false | -| alt-status-codes | array | no | | `title` @@ -1585,13 +1088,9 @@ The public facing URL of a monitored service, the user will be redirected here. The URL which will be requested and its response will determine the status of the site. If not specified, the `url` property is used. -`error-url` - -If the monitored service returns an error, the user will be redirected here. If not specified, the `url` property is used. - `icon` -Optional URL to an image which will be used as the icon for the site. Can be an external URL or internal via [server configured assets](#assets-path). You can also directly use [Simple Icons](https://simpleicons.org/) via a `si:` prefix or [Dashboard Icons](https://github.com/walkxcode/dashboard-icons) via a `di:` prefix: +Optional URL to an image which will be used as the icon for the site. Can be an external URL or internal via [server configured assets](#assets-path). You can also directly use [Simple Icons](https://simpleicons.org/) via a `si:` prefix: ```yaml icon: si:jellyfin @@ -1601,7 +1100,7 @@ icon: si:adguard > [!WARNING] > -> Simple Icons are loaded externally and are hosted on `cdn.jsdelivr.net`, if you do not wish to depend on a 3rd party you are free to download the icons individually and host them locally. +> Simple Icons are loaded externally and are hosted on `cdnjs.cloudflare.com`, if you do not wish to depend on a 3rd party you are free to download the icons individually and host them locally. `allow-insecure` @@ -1611,17 +1110,8 @@ Whether to ignore invalid/self-signed certificates. Whether to open the link in the same or a new tab. -`alt-status-codes` - -Status codes other than 200 that you want to return "OK". - -```yaml -alt-status-codes: - - 403 -``` - ### Releases -Display a list of latest releases for specific repositories on Github, GitLab, Codeberg or Docker Hub. +Display a list of latest releases for specific repositories on Github, GitLab or Docker Hub. Example: @@ -1632,7 +1122,6 @@ Example: - go-gitea/gitea - jellyfin/jellyfin - glanceapp/glance - - codeberg:redict/redict - gitlab:fdroid/fdroidclient - dockerhub:gotify/server ``` @@ -1653,16 +1142,15 @@ Preview: | collapse-after | integer | no | 5 | ##### `repositories` -A list of repositores to fetch the latest release for. Only the name/repo is required, not the full URL. A prefix can be specified for repositories hosted elsewhere such as GitLab, Codeberg and Docker Hub. Example: +A list of repositores to fetch the latest release for. Only the name/repo is required, not the full URL. A prefix can be specified for repositories hosted elsewhere such as GitLab and Docker Hub. Example: ```yaml repositories: - gitlab:inkscape/inkscape - dockerhub:glanceapp/glance - - codeberg:redict/redict ``` -Official images on Docker Hub can be specified by omitting the owner: +Official images on Docker Hub can be specified by ommiting the owner: ```yaml repositories: @@ -1671,7 +1159,7 @@ repositories: - dockerhub:alpine ``` -You can also specify exact tags for Docker Hub images: +You can also specify specific tags for Docker Hub images: ```yaml repositories: @@ -1679,20 +1167,9 @@ repositories: - dockerhub:nginx:stable-alpine ``` -To include prereleases you can specify the repository as an object and use the `include-prereleases` property: - -**Note: This feature is currently only available for GitHub repositories.** - -```yaml -repositories: - - gitlab:inkscape/inkscape - - repository: glanceapp/glance - include-prereleases: true - - codeberg:redict/redict -``` ##### `show-source-icon` -Shows an icon of the source (GitHub/GitLab/Codeberg/Docker Hub) next to the repository name when set to `true`. +Shows an icon of the source (GitHub/GitLab/Docker Hub) next to the repository name when set to `true`. ##### `token` Without authentication Github allows for up to 60 requests per hour. You can easily exceed this limit and start seeing errors if you're tracking lots of repositories or your cache time is low. To circumvent this you can [create a read only token from your Github account](https://github.com/settings/personal-access-tokens/new) and provide it here. @@ -1704,7 +1181,7 @@ services: glance: image: glanceapp/glance environment: - - GITHUB_TOKEN=docker-compose.yml
Success!
-{{ else }} -Failed to fetch data
-{{ end }} -``` - -You can also access the response headers: - -```html -{{ .String "name" }} is {{ .Int "age" }} years old
-{{ end }} -``` - -Output: - -```html -Steve is 30 years old
-Alex is 25 years old
-John is 35 years old
-``` - -For other ways of selecting data from a JSON Lines response, have a look at the docs for [tidwall/gjson](https://github.com/tidwall/gjson/tree/master?tab=readme-ov-file#json-lines). For example, to get an array of all names, you can use the following: - -```html -{{ range .JSON.Array "..#.name" }} -{{ .String "" }}
-{{ end }} -``` - -Output: - -```html -Steve
-Alex
-John
-``` - -## Functions - -The following functions are available on the `JSON` object: - -- `String(key string) string`: Returns the value of the key as a string. -- `Int(key string) int`: Returns the value of the key as an integer. -- `Float(key string) float`: Returns the value of the key as a float. -- `Bool(key string) bool`: Returns the value of the key as a boolean. -- `Array(key string) []JSON`: Returns the value of the key as an array of `JSON` objects. -- `Exists(key string) bool`: Returns true if the key exists in the JSON object. - -The following helper functions provided by Glance are available: - -- `toFloat(i int) float`: Converts an integer to a float. -- `toInt(f float) int`: Converts a float to an integer. -- `toRelativeTime(t time.Time) template.HTMLAttr`: Converts Time to a relative time such as 2h, 1d, etc which dynamically updates. **NOTE:** the value of this function should be used as an attribute in an HTML tag, e.g. ``. -- `now() time.Time`: Returns the current time. -- `offsetNow(offset string) time.Time`: Returns the current time with an offset. The offset can be positive or negative and must be in the format "3h" "-1h" or "2h30m10s". -- `duration(str string) time.Duration`: Parses a string such as `1h`, `24h`, `5h30m`, etc into a `time.Duration`. -- `parseTime(layout string, s string) time.Time`: Parses a string into time.Time. The layout must be provided in Go's [date format](https://pkg.go.dev/time#pkg-constants). You can alternatively use these values instead of the literal format: "unix", "RFC3339", "RFC3339Nano", "DateTime", "DateOnly". -- `parseRelativeTime(layout string, s string) time.Time`: A shorthand for `{{ .String "date" | parseTime "rfc3339" | toRelativeTime }}`. -- `add(a, b float) float`: Adds two numbers. -- `sub(a, b float) float`: Subtracts two numbers. -- `mul(a, b float) float`: Multiplies two numbers. -- `div(a, b float) float`: Divides two numbers. -- `formatApproxNumber(n int) string`: Formats a number to be more human-readable, e.g. 1000 -> 1k. -- `formatNumber(n float|int) string`: Formats a number with commas, e.g. 1000 -> 1,000. -- `trimPrefix(prefix string, str string) string`: Trims the prefix from a string. -- `trimSuffix(suffix string, str string) string`: Trims the suffix from a string. -- `trimSpace(str string) string`: Trims whitespace from a string on both ends. -- `replaceAll(old string, new string, str string) string`: Replaces all occurrences of a string in a string. -- `replaceMatches(pattern string, replacement string, str string) string`: Replaces all occurrences of a regular expression in a string. -- `findMatch(pattern string, str string) string`: Finds the first match of a regular expression in a string. -- `findSubmatch(pattern string, str string) string`: Finds the first submatch of a regular expression in a string. -- `sortByString(key string, order string, arr []JSON): []JSON`: Sorts an array of JSON objects by a string key in either ascending or descending order. -- `sortByInt(key string, order string, arr []JSON): []JSON`: Sorts an array of JSON objects by an integer key in either ascending or descending order. -- `sortByFloat(key string, order string, arr []JSON): []JSON`: Sorts an array of JSON objects by a float key in either ascending or descending order. -- `sortByTime(key string, layout string, order string, arr []JSON): []JSON`: Sorts an array of JSON objects by a time key in either ascending or descending order. The format must be provided in Go's [date format](https://pkg.go.dev/time#pkg-constants). -- `concat(strings ...string) string`: Concatenates multiple strings together. -- `unique(key string, arr []JSON) []JSON`: Returns a unique array of JSON objects based on the given key. - -The following helper functions provided by Go's `text/template` are available: - -- `eq(a, b any) bool`: Compares two values for equality. -- `ne(a, b any) bool`: Compares two values for inequality. -- `lt(a, b any) bool`: Compares two values for less than. -- `lte(a, b any) bool`: Compares two values for less than or equal to. -- `gt(a, b any) bool`: Compares two values for greater than. -- `gte(a, b any) bool`: Compares two values for greater than or equal to. -- `and(a, b bool) bool`: Returns true if both values are true. -- `or(a, b bool) bool`: Returns true if either value is true. -- `not(a bool) bool`: Returns the opposite of the value. -- `index(a any, b int) any`: Returns the value at the specified index of an array. -- `len(a any) int`: Returns the length of an array. -- `printf(format string, a ...any) string`: Returns a formatted string. diff --git a/docs/extensions.md b/docs/extensions.md index b6719c1..06db1ae 100644 --- a/docs/extensions.md +++ b/docs/extensions.md @@ -26,15 +26,9 @@ If you know how to setup an HTTP server and a bit of HTML and CSS you're ready t ### `Widget-Title` Used to specify the title of the widget. If not provided, the widget's title will be "Extension". -### `Widget-Title-URL` -Used to specify the URL that will be opened when the widget's title is clicked. If the user has specified a `title-url` in their config, it will take precedence over this header. - ### `Widget-Content-Type` Used to specify the content type that will be returned by the extension. If not provided, the content will be shown as plain text. -### `Widget-Content-Frameless` -When set to `true`, the widget's content will be displayed without the default background or "frame". - ## Content Types > [!NOTE] diff --git a/docs/glance.yml b/docs/glance.yml deleted file mode 100644 index 35dc7cb..0000000 --- a/docs/glance.yml +++ /dev/null @@ -1,105 +0,0 @@ -pages: - - name: Home - # Optionally, if you only have a single page you can hide the desktop navigation for a cleaner look - # hide-desktop-navigation: true - columns: - - size: small - widgets: - - type: calendar - first-day-of-week: monday - - - type: rss - limit: 10 - collapse-after: 3 - cache: 12h - feeds: - - url: https://selfh.st/rss/ - title: selfh.st - limit: 4 - - url: https://ciechanow.ski/atom.xml - - url: https://www.joshwcomeau.com/rss.xml - title: Josh Comeau - - url: https://samwho.dev/rss.xml - - url: https://ishadeed.com/feed.xml - title: Ahmad Shadeed - - - type: twitch-channels - channels: - - theprimeagen - - j_blow - - piratesoftware - - cohhcarnage - - christitustech - - EJ_SA - - - size: full - widgets: - - type: group - widgets: - - type: hacker-news - - type: lobsters - - - type: videos - channels: - - UCXuqSBlHAE6Xw-yeJA0Tunw # Linus Tech Tips - - UCR-DXc1voovS8nhAvccRZhg # Jeff Geerling - - UCsBjURrPoezykLs9EqgamOA # Fireship - - UCBJycsmduvYEL83R_U4JriQ # Marques Brownlee - - UCHnyfMqiRRG1u-2MsSQLbXA # Veritasium - - - type: group - widgets: - - type: reddit - subreddit: technology - show-thumbnails: true - - type: reddit - subreddit: selfhosted - show-thumbnails: true - - - size: small - widgets: - - type: weather - location: London, United Kingdom - units: metric # alternatively "imperial" - hour-format: 12h # alternatively "24h" - # Optionally hide the location from being displayed in the widget - # hide-location: true - - - type: markets - markets: - - symbol: SPY - name: S&P 500 - - symbol: BTC-USD - name: Bitcoin - - symbol: NVDA - name: NVIDIA - - symbol: AAPL - name: Apple - - symbol: MSFT - name: Microsoft - - - type: releases - cache: 1d - # Without authentication the Github API allows for up to 60 requests per hour. You can create a - # read-only token from your Github account settings and use it here to increase the limit. - # token: ... - repositories: - - glanceapp/glance - - go-gitea/gitea - - immich-app/immich - - syncthing/syncthing - - # Add more pages here: - # - name: Your page name - # columns: - # - size: small - # widgets: - # # Add widgets here - - # - size: full - # widgets: - # # Add widgets here - - # - size: small - # widgets: - # # Add widgets here diff --git a/docs/images/calendar-legacy-widget-preview.png b/docs/images/calendar-legacy-widget-preview.png deleted file mode 100644 index 5a161bf..0000000 Binary files a/docs/images/calendar-legacy-widget-preview.png and /dev/null differ diff --git a/docs/images/calendar-widget-preview.png b/docs/images/calendar-widget-preview.png index 4922a9b..5a161bf 100644 Binary files a/docs/images/calendar-widget-preview.png and b/docs/images/calendar-widget-preview.png differ diff --git a/docs/images/custom-api-preview-1.png b/docs/images/custom-api-preview-1.png deleted file mode 100644 index 4cf4c30..0000000 Binary files a/docs/images/custom-api-preview-1.png and /dev/null differ diff --git a/docs/images/custom-api-preview-2.png b/docs/images/custom-api-preview-2.png deleted file mode 100644 index 481ef85..0000000 Binary files a/docs/images/custom-api-preview-2.png and /dev/null differ diff --git a/docs/images/custom-api-preview-3.png b/docs/images/custom-api-preview-3.png deleted file mode 100644 index 15d8cb2..0000000 Binary files a/docs/images/custom-api-preview-3.png and /dev/null differ diff --git a/docs/images/docker-container-parent.png b/docs/images/docker-container-parent.png deleted file mode 100644 index 479b3e8..0000000 Binary files a/docs/images/docker-container-parent.png and /dev/null differ diff --git a/docs/images/docker-container-parent2.png b/docs/images/docker-container-parent2.png deleted file mode 100644 index 4562239..0000000 Binary files a/docs/images/docker-container-parent2.png and /dev/null differ diff --git a/docs/images/docker-containers-preview.png b/docs/images/docker-containers-preview.png deleted file mode 100644 index ba14fce..0000000 Binary files a/docs/images/docker-containers-preview.png and /dev/null differ diff --git a/docs/images/docker-widget-preview.png b/docs/images/docker-widget-preview.png deleted file mode 100644 index 5b644d4..0000000 Binary files a/docs/images/docker-widget-preview.png and /dev/null differ diff --git a/docs/images/monitor-widget-compact-preview.png b/docs/images/monitor-widget-compact-preview.png deleted file mode 100644 index 3e81fce..0000000 Binary files a/docs/images/monitor-widget-compact-preview.png and /dev/null differ diff --git a/docs/images/preconfigured-page-preview.png b/docs/images/preconfigured-page-preview.png index e10084c..0a57c14 100644 Binary files a/docs/images/preconfigured-page-preview.png and b/docs/images/preconfigured-page-preview.png differ diff --git a/docs/images/reddit-field-search.png b/docs/images/reddit-field-search.png index a84ee33..97ba04a 100644 Binary files a/docs/images/reddit-field-search.png and b/docs/images/reddit-field-search.png differ diff --git a/docs/images/server-stats-flame-icon.png b/docs/images/server-stats-flame-icon.png deleted file mode 100644 index 28cf0b8..0000000 Binary files a/docs/images/server-stats-flame-icon.png and /dev/null differ diff --git a/docs/images/server-stats-preview.gif b/docs/images/server-stats-preview.gif deleted file mode 100644 index 829679b..0000000 Binary files a/docs/images/server-stats-preview.gif and /dev/null differ diff --git a/docs/images/split-column-widget-3-columns.png b/docs/images/split-column-widget-3-columns.png deleted file mode 100644 index 88192c3..0000000 Binary files a/docs/images/split-column-widget-3-columns.png and /dev/null differ diff --git a/docs/images/split-column-widget-4-columns.png b/docs/images/split-column-widget-4-columns.png deleted file mode 100644 index 450f53e..0000000 Binary files a/docs/images/split-column-widget-4-columns.png and /dev/null differ diff --git a/docs/images/split-column-widget-masonry.png b/docs/images/split-column-widget-masonry.png deleted file mode 100644 index 145b2d6..0000000 Binary files a/docs/images/split-column-widget-masonry.png and /dev/null differ diff --git a/docs/images/split-column-widget-preview.png b/docs/images/split-column-widget-preview.png deleted file mode 100644 index 1ee336d..0000000 Binary files a/docs/images/split-column-widget-preview.png and /dev/null differ diff --git a/docs/images/themes/dracula.png b/docs/images/themes/dracula.png deleted file mode 100644 index 8dba452..0000000 Binary files a/docs/images/themes/dracula.png and /dev/null differ diff --git a/docs/images/themes/gruvbox.png b/docs/images/themes/gruvbox.png deleted file mode 100644 index 2e5b7a9..0000000 Binary files a/docs/images/themes/gruvbox.png and /dev/null differ diff --git a/docs/images/videos-widget-vertical-list-preview.png b/docs/images/videos-widget-vertical-list-preview.png deleted file mode 100644 index e33ce86..0000000 Binary files a/docs/images/videos-widget-vertical-list-preview.png and /dev/null differ diff --git a/docs/preconfigured-pages.md b/docs/preconfigured-pages.md index b70610b..b382917 100644 --- a/docs/preconfigured-pages.md +++ b/docs/preconfigured-pages.md @@ -4,10 +4,6 @@ Don't want to spend time configuring pages from scratch? No problem! Simply copy Pull requests with your page configurations are welcome! -> [!NOTE] -> -> Pages must be placed under a top level `pages:` key, you can read more about that [here](configuration.md#pages). - ## Startpage  diff --git a/docs/themes.md b/docs/themes.md index fdc10b2..b4185db 100644 --- a/docs/themes.md +++ b/docs/themes.md @@ -53,16 +53,6 @@ theme: primary-color: 97 13 80 ``` -### Gruvbox Dark - -```yaml -theme: - background-color: 0 0 16 - primary-color: 43 59 81 - positive-color: 61 66 44 - negative-color: 6 96 59 -``` - ### Kanagawa Dark  ```yaml @@ -82,17 +72,6 @@ theme: negative-color: 209 88 54 ``` -### Dracula - -```yaml -theme: - background-color: 231 15 21 - primary-color: 265 89 79 - contrast-multiplier: 1.2 - positive-color: 135 94 66 - negative-color: 0 100 67 -``` - ## Light ### Catppuccin Latte diff --git a/docs/v0.7.0-upgrade.md b/docs/v0.7.0-upgrade.md deleted file mode 100644 index 93fa6b8..0000000 --- a/docs/v0.7.0-upgrade.md +++ /dev/null @@ -1,57 +0,0 @@ -## Upgrading to v0.7.0 from previous versions - -In essence, the `glance.yml` file has been moved from the root of the project to a `config/` directory and you now need to mount that directory to `/app/config` in the container. - -### Before - -Versions before v0.7.0 used a `docker-compose.yml` that looked like the following: - -```yaml -services: - glance: - image: glanceapp/glance - volumes: - - ./glance.yml:/app/glance.yml - ports: - - 8080:8080 -``` - -And expected you to have the following directory structure: - -```plaintext -glance/ - docker-compose.yml - glance.yml -``` - -### After - -With the release of v0.7.0, the recommended `docker-compose.yml` looks like the following: - -```yaml -services: - glance: - container_name: glance - image: glanceapp/glance - volumes: - - ./config:/app/config - ports: - - 8080:8080 -``` - -And expects you to have the following directory structure: - -```plaintext -glance/ - docker-compose.yml - config/ - glance.yml -``` - -## Why this change was necessary - -1. Mounting a file rather than a directory is not common practice and leads to some issues, such as creating a directory if the file is not present, which has tripped up multiple people and caused unnecessary confusion -2. v0.7.0 added automatic reloads when the configuration file changes, which based on testing didn't work when mounting a single file -3. v0.7.0 added the ability to include config files, so you'd have to make this change anyways if you wanted to take advantage of that feature - -Taking all of these into account, it felt like the right time to implement the change. diff --git a/go.mod b/go.mod index 4c19477..17aa4d4 100644 --- a/go.mod +++ b/go.mod @@ -1,32 +1,19 @@ module github.com/glanceapp/glance -go 1.24.2 +go 1.22.5 require ( - github.com/fsnotify/fsnotify v1.9.0 github.com/mmcdole/gofeed v1.3.0 - github.com/shirou/gopsutil/v4 v4.25.3 - github.com/tidwall/gjson v1.18.0 - golang.org/x/text v0.24.0 + golang.org/x/text v0.16.0 gopkg.in/yaml.v3 v3.0.1 ) require ( - github.com/PuerkitoBio/goquery v1.10.2 // indirect - github.com/andybalholm/cascadia v1.3.3 // indirect - github.com/ebitengine/purego v0.8.2 // indirect - github.com/go-ole/go-ole v1.3.0 // indirect + github.com/PuerkitoBio/goquery v1.9.2 // indirect + github.com/andybalholm/cascadia v1.3.2 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect github.com/mmcdole/goxpp v1.1.1 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect - github.com/tidwall/match v1.1.1 // indirect - github.com/tidwall/pretty v1.2.1 // indirect - github.com/tklauser/go-sysconf v0.3.15 // indirect - github.com/tklauser/numcpus v0.10.0 // indirect - github.com/yusufpapurcu/wmi v1.2.4 // indirect - golang.org/x/net v0.39.0 // indirect - golang.org/x/sys v0.32.0 // indirect + golang.org/x/net v0.27.0 // indirect ) diff --git a/go.sum b/go.sum index 9a79559..28cb1ae 100644 --- a/go.sum +++ b/go.sum @@ -1,31 +1,13 @@ -github.com/PuerkitoBio/goquery v1.10.1 h1:Y8JGYUkXWTGRB6Ars3+j3kN0xg1YqqlwvdTV8WTFQcU= -github.com/PuerkitoBio/goquery v1.10.1/go.mod h1:IYiHrOMps66ag56LEH7QYDDupKXyo5A8qrjIx3ZtujY= -github.com/PuerkitoBio/goquery v1.10.2 h1:7fh2BdHcG6VFZsK7toXBT/Bh1z5Wmy8Q9MV9HqT2AM8= -github.com/PuerkitoBio/goquery v1.10.2/go.mod h1:0guWGjcLu9AYC7C1GHnpysHy056u9aEkUHwhdnePMCU= -github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM= -github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA= +github.com/PuerkitoBio/goquery v1.9.2 h1:4/wZksC3KgkQw7SQgkKotmKljk0M6V8TUvA8Wb4yPeE= +github.com/PuerkitoBio/goquery v1.9.2/go.mod h1:GHPCaP0ODyyxqcNoFGYlAprUFH81NuRPd0GX3Zu2Mvk= +github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss= +github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I= -github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= -github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= -github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= -github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= -github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= -github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 h1:7UMa6KCCMjZEMDtTVdcGu0B1GmmC7QJKiCCjyTAWQy0= -github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k= -github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 h1:PpXWgLPs+Fqr325bN2FD2ISlRRztXibcX6e8f5FR5Dc= -github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= github.com/mmcdole/gofeed v1.3.0 h1:5yn+HeqlcvjMeAI4gu6T+crm7d0anY85+M+v6fIFNG4= github.com/mmcdole/gofeed v1.3.0/go.mod h1:9TGv2LcJhdXePDzxiuMnukhV2/zb6VtnZt1mS+SjkLE= github.com/mmcdole/goxpp v1.1.1 h1:RGIX+D6iQRIunGHrKqnA2+700XMCnNv0bAOOv5MUhx8= @@ -37,111 +19,47 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= -github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= -github.com/shirou/gopsutil/v4 v4.25.1 h1:QSWkTc+fu9LTAWfkZwZ6j8MSUk4A2LV7rbH0ZqmLjXs= -github.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI= -github.com/shirou/gopsutil/v4 v4.25.3 h1:SeA68lsu8gLggyMbmCn8cmp97V1TI9ld9sVzAUcKcKE= -github.com/shirou/gopsutil/v4 v4.25.3/go.mod h1:xbuxyoZj+UsgnZrENu3lQivsngRR5BdjbJwf2fv4szA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= -github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= -github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= -github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= -github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= -github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= -github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= -github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU= -github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY= -github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= -github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= -github.com/tklauser/numcpus v0.9.0 h1:lmyCHtANi8aRUgkckBgoDk1nHCux3n2cgkJLXdQGPDo= -github.com/tklauser/numcpus v0.9.0/go.mod h1:SN6Nq1O3VychhC1npsWostA+oW+VOQTxZrS604NSRyI= -github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= -github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= -github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= -golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= -golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= -golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= -golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= -golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= -golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= +golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= -golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= -golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= -golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= +golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= -golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= -golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= -golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/assets/files.go b/internal/assets/files.go new file mode 100644 index 0000000..2c7c09e --- /dev/null +++ b/internal/assets/files.go @@ -0,0 +1,56 @@ +package assets + +import ( + "crypto/md5" + "embed" + "encoding/hex" + "io" + "io/fs" + "log/slog" + "strconv" + "time" +) + +//go:embed static +var _publicFS embed.FS + +//go:embed templates +var _templateFS embed.FS + +var PublicFS, _ = fs.Sub(_publicFS, "static") +var TemplateFS, _ = fs.Sub(_templateFS, "templates") + +func getFSHash(files fs.FS) string { + hash := md5.New() + + err := fs.WalkDir(files, ".", func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + + if d.IsDir() { + return nil + } + + file, err := files.Open(path) + + if err != nil { + return err + } + + if _, err := io.Copy(hash, file); err != nil { + return err + } + + return nil + }) + + if err == nil { + return hex.EncodeToString(hash.Sum(nil))[:10] + } + + slog.Warn("Could not compute assets cache", "err", err) + return strconv.FormatInt(time.Now().Unix(), 10) +} + +var PublicFSHash = getFSHash(PublicFS) diff --git a/internal/glance/static/app-icon.png b/internal/assets/static/app-icon.png similarity index 100% rename from internal/glance/static/app-icon.png rename to internal/assets/static/app-icon.png diff --git a/internal/glance/static/favicon.png b/internal/assets/static/favicon.png similarity index 100% rename from internal/glance/static/favicon.png rename to internal/assets/static/favicon.png diff --git a/internal/glance/static/fonts/JetBrainsMono-Regular.woff2 b/internal/assets/static/fonts/JetBrainsMono-Regular.woff2 similarity index 100% rename from internal/glance/static/fonts/JetBrainsMono-Regular.woff2 rename to internal/assets/static/fonts/JetBrainsMono-Regular.woff2 diff --git a/internal/glance/static/icons/dockerhub.svg b/internal/assets/static/icons/dockerhub.svg similarity index 100% rename from internal/glance/static/icons/dockerhub.svg rename to internal/assets/static/icons/dockerhub.svg diff --git a/internal/glance/static/icons/github.svg b/internal/assets/static/icons/github.svg similarity index 100% rename from internal/glance/static/icons/github.svg rename to internal/assets/static/icons/github.svg diff --git a/internal/glance/static/icons/gitlab.svg b/internal/assets/static/icons/gitlab.svg similarity index 100% rename from internal/glance/static/icons/gitlab.svg rename to internal/assets/static/icons/gitlab.svg diff --git a/internal/glance/static/js/main.js b/internal/assets/static/js/main.js similarity index 80% rename from internal/glance/static/js/main.js rename to internal/assets/static/js/main.js index 41d2ae3..228f57d 100644 --- a/internal/glance/static/js/main.js +++ b/internal/assets/static/js/main.js @@ -1,6 +1,27 @@ import { setupPopovers } from './popover.js'; -import { setupMasonries } from './masonry.js'; -import { throttledDebounce, isElementVisible, openURLInNewTab } from './utils.js'; + +function throttledDebounce(callback, maxDebounceTimes, debounceDelay) { + let debounceTimeout; + let timesDebounced = 0; + + return function () { + if (timesDebounced == maxDebounceTimes) { + clearTimeout(debounceTimeout); + timesDebounced = 0; + callback(); + return; + } + + clearTimeout(debounceTimeout); + timesDebounced++; + + debounceTimeout = setTimeout(() => { + timesDebounced = 0; + callback(); + }, debounceDelay); + }; +}; + async function fetchPageContent(pageData) { // TODO: handle non 200 status codes/time outs @@ -49,35 +70,29 @@ function setupCarousels() { const minuteInSeconds = 60; const hourInSeconds = minuteInSeconds * 60; const dayInSeconds = hourInSeconds * 24; -const monthInSeconds = dayInSeconds * 30.4; -const yearInSeconds = dayInSeconds * 365; +const monthInSeconds = dayInSeconds * 30; +const yearInSeconds = monthInSeconds * 12; -function timestampToRelativeTime(timestamp) { - let delta = Math.round((Date.now() / 1000) - timestamp); - let prefix = ""; - - if (delta < 0) { - delta = -delta; - prefix = "in "; - } +function relativeTimeSince(timestamp) { + const delta = Math.round((Date.now() / 1000) - timestamp); if (delta < minuteInSeconds) { - return prefix + "1m"; + return "1m"; } if (delta < hourInSeconds) { - return prefix + Math.floor(delta / minuteInSeconds) + "m"; + return Math.floor(delta / minuteInSeconds) + "m"; } if (delta < dayInSeconds) { - return prefix + Math.floor(delta / hourInSeconds) + "h"; + return Math.floor(delta / hourInSeconds) + "h"; } if (delta < monthInSeconds) { - return prefix + Math.floor(delta / dayInSeconds) + "d"; + return Math.floor(delta / dayInSeconds) + "d"; } if (delta < yearInSeconds) { - return prefix + Math.floor(delta / monthInSeconds) + "mo"; + return Math.floor(delta / monthInSeconds) + "mo"; } - return prefix + Math.floor(delta / yearInSeconds) + "y"; + return Math.floor(delta / yearInSeconds) + "y"; } function updateRelativeTimeForElements(elements) @@ -90,7 +105,7 @@ function updateRelativeTimeForElements(elements) if (timestamp === undefined) continue - element.textContent = timestampToRelativeTime(timestamp); + element.textContent = relativeTimeSince(timestamp); } } @@ -111,7 +126,6 @@ function setupSearchBoxes() { const bangsMap = {}; const kbdElement = widget.getElementsByTagName("kbd")[0]; let currentBang = null; - let lastQuery = ""; for (let j = 0; j < bangs.length; j++) { const bang = bangs[j]; @@ -148,14 +162,6 @@ function setupSearchBoxes() { window.location.href = url; } - lastQuery = query; - inputElement.value = ""; - - return; - } - - if (event.key == "ArrowUp" && lastQuery.length > 0) { - inputElement.value = lastQuery; return; } }; @@ -261,32 +267,14 @@ function setupGroups() { for (let t = 0; t < titles.length; t++) { const title = titles[t]; - - if (title.dataset.titleUrl !== undefined) { - title.addEventListener("mousedown", (event) => { - if (event.button != 1) { - return; - } - - openURLInNewTab(title.dataset.titleUrl, false); - event.preventDefault(); - }); - } - title.addEventListener("click", () => { if (t == current) { - if (title.dataset.titleUrl !== undefined) { - openURLInNewTab(title.dataset.titleUrl); - } - return; } for (let i = 0; i < titles.length; i++) { titles[i].classList.remove("widget-group-title-current"); - titles[i].setAttribute("aria-selected", "false"); tabs[i].classList.remove("widget-group-content-current"); - tabs[i].setAttribute("aria-hidden", "true"); } if (current < t) { @@ -298,9 +286,7 @@ function setupGroups() { current = t; title.classList.add("widget-group-title-current"); - title.setAttribute("aria-selected", "true"); tabs[t].classList.add("widget-group-content-current"); - tabs[t].setAttribute("aria-hidden", "false"); }); } } @@ -441,9 +427,9 @@ function setupCollapsibleGrids() { const button = attachExpandToggleButton(gridElement); - let cardsPerRow; + let cardsPerRow = 2; - const resolveCollapsibleItems = () => requestAnimationFrame(() => { + const resolveCollapsibleItems = () => { const hideItemsAfterIndex = cardsPerRow * collapseAfterRows; if (hideItemsAfterIndex >= gridElement.children.length) { @@ -469,13 +455,14 @@ function setupCollapsibleGrids() { child.style.removeProperty("animation-delay"); } } + }; + + afterContentReady(() => { + cardsPerRow = getCardsPerRow(); + resolveCollapsibleItems(); }); - const observer = new ResizeObserver(() => { - if (!isElementVisible(gridElement)) { - return; - } - + window.addEventListener("resize", () => { const newCardsPerRow = getCardsPerRow(); if (cardsPerRow == newCardsPerRow) { @@ -485,8 +472,6 @@ function setupCollapsibleGrids() { cardsPerRow = newCardsPerRow; resolveCollapsibleItems(); }); - - afterContentReady(() => observer.observe(gridElement)); } } @@ -538,34 +523,9 @@ function timeInZone(now, zone) { timeInZone = now } - const diffInMinutes = Math.round((timeInZone.getTime() - now.getTime()) / 1000 / 60); + const diffInHours = Math.round((timeInZone.getTime() - now.getTime()) / 1000 / 60 / 60); - return { time: timeInZone, diffInMinutes: diffInMinutes }; -} - -function zoneDiffText(diffInMinutes) { - if (diffInMinutes == 0) { - return ""; - } - - const sign = diffInMinutes < 0 ? "-" : "+"; - const signText = diffInMinutes < 0 ? "behind" : "ahead"; - - diffInMinutes = Math.abs(diffInMinutes); - - const hours = Math.floor(diffInMinutes / 60); - const minutes = diffInMinutes % 60; - const hourSuffix = hours == 1 ? "" : "s"; - - if (minutes == 0) { - return { text: `${sign}${hours}h`, title: `${hours} hour${hourSuffix} ${signText}` }; - } - - if (hours == 0) { - return { text: `${sign}${minutes}m`, title: `${minutes} minutes ${signText}` }; - } - - return { text: `${sign}${hours}h~`, title: `${hours} hour${hourSuffix} and ${minutes} minutes ${signText}` }; + return { time: timeInZone, diffInHours: diffInHours }; } function setupClocks() { @@ -608,11 +568,9 @@ function setupClocks() { ); updateCallbacks.push((now) => { - const { time, diffInMinutes } = timeInZone(now, timeZoneContainer.dataset.timeInZone); + const { time, diffInHours } = timeInZone(now, timeZoneContainer.dataset.timeInZone); setZoneTime(time); - const { text, title } = zoneDiffText(diffInMinutes); - diffElement.textContent = text; - diffElement.title = title; + diffElement.textContent = (diffInHours <= 0 ? diffInHours : '+' + diffInHours) + 'h'; }); } } @@ -629,30 +587,6 @@ function setupClocks() { updateClocks(); } -async function setupCalendars() { - const elems = document.getElementsByClassName("calendar"); - if (elems.length == 0) return; - - // TODO: implement prefetching, currently loads as a nasty waterfall of requests - const calendar = await import ('./calendar.js'); - - for (let i = 0; i < elems.length; i++) - calendar.default(elems[i]); -} - -function setupTruncatedElementTitles() { - const elements = document.querySelectorAll(".text-truncate, .single-line-titles .title, .text-truncate-2-lines, .text-truncate-3-lines"); - - if (elements.length == 0) { - return; - } - - for (let i = 0; i < elements.length; i++) { - const element = elements[i]; - if (element.getAttribute("title") === null) element.title = element.textContent; - } -} - async function setupPage() { const pageElement = document.getElementById("page"); const pageContentElement = document.getElementById("page-content"); @@ -663,27 +597,20 @@ async function setupPage() { try { setupPopovers(); setupClocks() - await setupCalendars(); setupCarousels(); setupSearchBoxes(); setupCollapsibleLists(); setupCollapsibleGrids(); setupGroups(); - setupMasonries(); setupDynamicRelativeTime(); setupLazyImages(); } finally { pageElement.classList.add("content-ready"); - pageElement.setAttribute("aria-busy", "false"); for (let i = 0; i < contentReadyCallbacks.length; i++) { contentReadyCallbacks[i](); } - setTimeout(() => { - setupTruncatedElementTitles(); - }, 50); - setTimeout(() => { document.body.classList.add("page-columns-transitioned"); }, 300); diff --git a/internal/glance/static/js/popover.js b/internal/assets/static/js/popover.js similarity index 90% rename from internal/glance/static/js/popover.js rename to internal/assets/static/js/popover.js index 331ee26..d6578ee 100644 --- a/internal/glance/static/js/popover.js +++ b/internal/assets/static/js/popover.js @@ -25,8 +25,7 @@ frameElement.append(contentElement); containerElement.append(frameElement); document.body.append(containerElement); -const queueRepositionContainer = () => requestAnimationFrame(repositionContainer); -const observer = new ResizeObserver(queueRepositionContainer); +const observer = new ResizeObserver(repositionContainer); function handleMouseEnter(event) { clearTogglePopoverTimeout(); @@ -57,8 +56,6 @@ function clearTogglePopoverTimeout() { } function showPopover() { - if (pendingTarget === null) return; - activeTarget = pendingTarget; pendingTarget = null; @@ -98,25 +95,23 @@ function showPopover() { } contentElement.style.maxWidth = contentMaxWidth; + containerElement.style.display = "block"; activeTarget.classList.add("popover-active"); document.addEventListener("keydown", handleHidePopoverOnEscape); - window.addEventListener("resize", queueRepositionContainer); + window.addEventListener("resize", repositionContainer); observer.observe(containerElement); } function repositionContainer() { - containerElement.style.display = "block"; - const targetBounds = activeTarget.dataset.popoverAnchor !== undefined ? activeTarget.querySelector(activeTarget.dataset.popoverAnchor).getBoundingClientRect() : activeTarget.getBoundingClientRect(); const containerBounds = containerElement.getBoundingClientRect(); const containerInlinePadding = parseInt(containerComputedStyle.getPropertyValue("padding-inline")); - const targetBoundsWidthOffset = targetBounds.width * (activeTarget.dataset.popoverTargetOffset || 0.5); + const targetBoundsWidthOffset = targetBounds.width * (activeTarget.dataset.popoverOffset || 0.5); const position = activeTarget.dataset.popoverPosition || "below"; - const popoverOffest = activeTarget.dataset.popoverOffset || 0.5; - const left = Math.round(targetBounds.left + targetBoundsWidthOffset - (containerBounds.width * popoverOffest)); + const left = Math.round(targetBounds.left + targetBoundsWidthOffset - (containerBounds.width / 2)); if (left < 0) { containerElement.style.left = 0; @@ -125,11 +120,11 @@ function repositionContainer() { } else if (left + containerBounds.width > window.innerWidth) { containerElement.style.removeProperty("left"); containerElement.style.right = 0; - containerElement.style.setProperty("--triangle-offset", containerBounds.width - containerInlinePadding - (window.innerWidth - targetBounds.left - targetBoundsWidthOffset) + -1 + "px"); + containerElement.style.setProperty("--triangle-offset", containerBounds.width - containerInlinePadding - (window.innerWidth - targetBounds.left - targetBoundsWidthOffset) + "px"); } else { containerElement.style.removeProperty("right"); containerElement.style.left = left + "px"; - containerElement.style.setProperty("--triangle-offset", ((targetBounds.left + targetBoundsWidthOffset) - left - containerInlinePadding) + -1 + "px"); + containerElement.style.removeProperty("--triangle-offset"); } const distanceFromTarget = activeTarget.dataset.popoverMargin || defaultDistanceFromTarget; @@ -158,7 +153,7 @@ function hidePopover() { activeTarget.classList.remove("popover-active"); containerElement.style.display = "none"; document.removeEventListener("keydown", handleHidePopoverOnEscape); - window.removeEventListener("resize", queueRepositionContainer); + window.removeEventListener("resize", repositionContainer); observer.unobserve(containerElement); if (cleanupOnHidePopover !== null) { diff --git a/internal/glance/static/main.css b/internal/assets/static/main.css similarity index 86% rename from internal/glance/static/main.css rename to internal/assets/static/main.css index 2975a73..c2a4acd 100644 --- a/internal/glance/static/main.css +++ b/internal/assets/static/main.css @@ -17,7 +17,7 @@ --cm: 1; --tsm: 1; - --widget-gap: 23px; + --widget-gap: 25px; --widget-content-vertical-padding: 15px; --widget-content-horizontal-padding: 17px; --widget-content-padding: var(--widget-content-vertical-padding) var(--widget-content-horizontal-padding); @@ -37,14 +37,13 @@ --color-popover-background: hsl(var(--bgh), calc(var(--bgs) + 3%), calc(var(--bgl) + 3%)); --color-popover-border: hsl(var(--bghs), calc(var(--scheme) (var(--scheme) var(--bgl) + 12%))); --color-progress-border: hsl(var(--bghs), calc(var(--scheme) ((var(--scheme) var(--bgl)) + 10% * var(--cm)))); - --color-progress-value: hsl(var(--bgh), calc(var(--bgs) * var(--tsm)), calc(var(--scheme) ((var(--scheme) var(--bgl)) + 30% * var(--cm)))); + --color-progress-value: hsl(var(--bgh), calc(var(--bgs) * var(--tsm)), calc(var(--scheme) ((var(--scheme) var(--bgl)) + 27% * var(--cm)))); --color-graph-gridlines: hsl(var(--bghs), calc(var(--scheme) ((var(--scheme) var(--bgl)) + 6% * var(--cm)))); --ths: var(--bgh), calc(var(--bgs) * var(--tsm)); - --color-text-highlight: hsl(var(--ths), calc(var(--scheme) var(--cm) * 85%)); - --color-text-paragraph: hsl(var(--ths), calc(var(--scheme) var(--cm) * 73%)); --color-text-base: hsl(var(--ths), calc(var(--scheme) var(--cm) * 58%)); --color-text-base-muted: hsl(var(--ths), calc(var(--scheme) var(--cm) * 52%)); + --color-text-highlight: hsl(var(--ths), calc(var(--scheme) var(--cm) * 85%)); --color-text-subdue: hsl(var(--ths), calc(var(--scheme) var(--cm) * 35%)); --font-size-h1: 1.7rem; @@ -110,7 +109,7 @@ .visited-indicator:not(.text-truncate)::after, .visited-indicator.text-truncate::before, .bookmarks-link:not(.bookmarks-link-no-arrow)::after { - content: '↗' / ""; + content: '↗'; margin-left: 0.5em; display: inline-block; position: relative; @@ -189,7 +188,7 @@ } .expand-toggle-button-icon::before { - content: '' / ""; + content: ''; font-size: 0.8rem; transform: rotate(90deg); line-height: 1; @@ -274,10 +273,6 @@ background-color: var(--color-separator); } -pre { - font: inherit; -} - ::selection { background-color: hsl(var(--bghs), calc(var(--scheme) (var(--scheme) var(--bgl) + 20%))); color: var(--color-text-highlight); @@ -294,12 +289,6 @@ pre { width: 10px; } -*:focus-visible { - outline: 2px solid var(--color-primary); - outline-offset: 0.1rem; - border-radius: var(--border-radius); -} - *, *::before, *::after { box-sizing: border-box; } @@ -337,23 +326,10 @@ html { scroll-behavior: smooth; } -html, body, .body-content { +html, body { height: 100%; } -h1, h2, h3, h4, h5 { - font: inherit; -} - -.visually-hidden { - clip-path: inset(50%); - height: 1px; - overflow: hidden; - position: absolute; - white-space: nowrap; - width: 1px; -} - a { text-decoration: none; color: inherit; @@ -464,17 +440,6 @@ kbd:active { box-shadow: 0 0 0 0 var(--color-widget-background-highlight); } -.masonry { - display: flex; - gap: var(--widget-gap); -} - -.masonry-column { - flex: 1; - display: flex; - flex-direction: column; -} - .popover-container, [data-popover-html] { display: none; } @@ -549,11 +514,6 @@ kbd:active { list-style: none; position: relative; display: flex; - z-index: 1; -} - -.summary::-webkit-details-marker { - display: none; } .details[open] .summary { @@ -575,12 +535,8 @@ kbd:active { opacity: 1; } -.details:not([open]) .list-with-transition { - display: none; -} - .summary::after { - content: "◀" / ""; + content: "◀"; font-size: 1.2em; position: absolute; top: 0; @@ -751,7 +707,6 @@ details[open] .summary::after { justify-content: space-between; position: relative; margin-bottom: 1.8rem; - z-index: 1; } .widget-error-header::before { @@ -767,11 +722,19 @@ details[open] .summary::after { .widget-error-icon { width: 2.4rem; height: 2.4rem; + border: 0.2rem solid var(--color-negative); + border-radius: 50%; + text-align: center; + line-height: 2rem; flex-shrink: 0; - stroke: var(--color-negative); opacity: 0.6; } +.widget-error-icon::before { + content: '!'; + color: var(--color-text-highlight); +} + .widget-content { container-type: inline-size; container-name: widget; @@ -813,20 +776,6 @@ details[open] .summary::after { gap: 1rem; } -.widget-beta-icon { - width: 1.6rem; - height: 1.6rem; - flex-shrink: 0; - transition: transform .45s, opacity .45s, stroke .45s; - opacity: 0.7; -} - -.widget-beta-icon:hover, .widget-header .popover-active > .widget-beta-icon { - fill: var(--color-text-highlight); - transform: translateY(-10%) scale(1.3); - opacity: 1; -} - .widget + .widget { margin-top: var(--widget-gap); } @@ -839,9 +788,9 @@ details[open] .summary::after { } .list-horizontal-text > *:not(:last-child)::after { - content: '•' / ""; + content: '•'; color: var(--color-text-subdue); - margin: 0 0.4rem; + margin: 0 0.5rem; position: relative; top: 0.1rem; } @@ -902,7 +851,6 @@ details[open] .summary::after { border-bottom: 2px solid transparent; transition: color .3s, border-color .3s; font-size: var(--font-size-h3); - flex-shrink: 0; } .nav-item:not(.nav-item-current):hover { @@ -973,11 +921,15 @@ details[open] .summary::after { border-radius: var(--border-radius) var(--border-radius) 0 0; } -.video-horizontal-list-thumbnail { - height: 4rem; - aspect-ratio: 16 / 8.9; - object-fit: cover; - border-radius: var(--border-radius); +.video-title { + margin-bottom: auto; + overflow: hidden; + display: block; + text-overflow: ellipsis; + line-clamp: 2; + -webkit-line-clamp: 2; + display: -webkit-box; + -webkit-box-orient: vertical; } .search-icon { @@ -1060,6 +1012,11 @@ details[open] .summary::after { display: none; } +.forum-post-list-item { + display: flex; + gap: 1.2rem; +} + .forum-post-list-thumbnail { flex-shrink: 0; width: 6rem; @@ -1074,12 +1031,6 @@ details[open] .summary::after { transform: translateY(-0.15rem); } -@container widget (max-width: 550px) { - .forum-post-autohide { - display: none; - } -} - .bookmarks-group { --bookmarks-group-color: var(--color-primary); } @@ -1098,7 +1049,6 @@ details[open] .summary::after { border-radius: var(--border-radius); padding: 0.5rem; opacity: 0.7; - flex-shrink: 0; } .bookmarks-icon { @@ -1107,82 +1057,23 @@ details[open] .summary::after { opacity: 0.8; } -:root:not(.light-scheme) .flat-icon { +:root:not(.light-scheme) .simple-icon { filter: invert(1); } -.old-calendar-day { +.calendar-day { width: calc(100% / 7); text-align: center; padding: 0.6rem 0; } -.old-calendar-day-today { + +.calendar-day-today { border-radius: var(--border-radius); background-color: hsl(var(--bghs), calc(var(--scheme) (var(--scheme) (var(--bgl)) + 6%))); color: var(--color-text-highlight); } -.calendar-dates { - text-align: center; - display: grid; - grid-template-columns: repeat(7, 1fr); - gap: 2px; -} - -.calendar-date { - padding: 0.4rem 0; - color: var(--color-text-base); - position: relative; - border-radius: var(--border-radius); - background: none; - border: none; - font: inherit; -} - -.calendar-current-date { - border-radius: var(--border-radius); - background-color: var(--color-popover-border); - color: var(--color-text-highlight); -} - -.calendar-spillover-date { - color: var(--color-text-subdue); -} - -.calendar-header-button { - position: relative; - cursor: pointer; - width: 2rem; - height: 2rem; - z-index: 1; - background: none; - border: none; -} - -.calendar-header-button::before { - content: ''; - position: absolute; - inset: -0.2rem; - border-radius: var(--border-radius); - background-color: var(--color-text-subdue); - opacity: 0; - transition: opacity 0.2s; - z-index: -1; -} - -.calendar-header-button:hover::before { - opacity: 0.4; -} - -.calendar-undo-button { - display: inline-block; - vertical-align: text-top; - width: 2rem; - height: 2rem; - margin-left: 0.7rem; -} - .dns-stats-totals { transition: opacity .3s; transition-delay: 50ms; @@ -1202,6 +1093,7 @@ details[open] .summary::after { .dns-stats-graph-gridlines-container { position: absolute; + z-index: -1; inset: 0; } @@ -1228,6 +1120,7 @@ details[open] .summary::after { content: ''; position: absolute; inset: 1px 0; + z-index: -1; opacity: 0; background: var(--color-text-base); transition: opacity .2s; @@ -1273,7 +1166,6 @@ details[open] .summary::after { .dns-stats-graph-bar > .blocked { background-color: var(--color-negative); - flex-basis: calc(var(--percent) - 1px); } .dns-stats-graph-column:nth-child(even) .dns-stats-graph-time { @@ -1370,6 +1262,7 @@ details[open] .summary::after { overflow: hidden; mask-image: linear-gradient(0deg, transparent 40%, #000); -webkit-mask-image: linear-gradient(0deg, transparent 40%, #000); + z-index: -1; } .weather-column-rain::before { @@ -1444,10 +1337,6 @@ details[open] .summary::after { transform: translate(-50%, -50%); } -.clock-time { - min-width: 8ch; -} - .clock-time span { color: var(--color-text-highlight); } @@ -1464,7 +1353,7 @@ details[open] .summary::after { transition: filter 0.3s, opacity 0.3s; } -.monitor-site-icon.flat-icon { +.monitor-site-icon.simple-icon { opacity: 0.7; } @@ -1472,7 +1361,7 @@ details[open] .summary::after { opacity: 1; } -.monitor-site:hover .monitor-site-icon:not(.flat-icon) { +.monitor-site:hover .monitor-site-icon:not(.simple-icon) { filter: grayscale(0); } @@ -1483,170 +1372,6 @@ details[open] .summary::after { height: 2rem; } -.monitor-site-status-icon-compact { - width: 1.8rem; - height: 1.8rem; - flex-shrink: 0; -} - -.docker-container-icon { - display: block; - filter: grayscale(0.4); - object-fit: contain; - aspect-ratio: 1 / 1; - width: 2.7rem; - opacity: 0.8; - transition: filter 0.3s, opacity 0.3s; -} - -.docker-container-icon.flat-icon { - opacity: 0.7; -} - -.docker-container:hover .docker-container-icon { - opacity: 1; -} - -.docker-container:hover .docker-container-icon:not(.flat-icon) { - filter: grayscale(0); -} - -.docker-container-status-icon { - width: 2rem; - height: 2rem; -} - -.widget-type-server-info { - position: relative; -} - -.server + .server { - margin-top: 3rem; -} - -.server { - gap: 1rem; - display: flex; - flex-direction: column; -} - -.server-info { - align-items: center; - display: flex; - justify-content: space-between; - gap: 1.5rem; - flex-shrink: 1; - min-width: 0; -} - -.server-details { - min-width: 0; -} - -.server-icon { - height: 3rem; - width: 3rem; -} - -.server-spicy-cpu-icon { - height: 1em; - align-self: center; - margin-left: 0.4em; - margin-bottom: 0.2rem; -} - -.server-stats { - display: flex; - gap: 1.5rem; - margin-top: 0.5rem; -} - -.server-stat-unavailable { - opacity: 0.5; -} - -.progress-bar { - border: 1px solid var(--color-progress-border); - border-radius: var(--border-radius); - display: flex; - flex-direction: column; - gap: 2px; - padding: 2px; - height: 1.5rem; - margin-inline: -3px; /* naughty, but oh so beautiful */ -} - -.progress-bar-combined { - height: 3rem; -} - -.popover-active > .progress-bar { - transition: border-color .3s; - border-color: var(--color-text-subdue); -} - -.progress-value { - --half-border-radius: calc(var(--border-radius) / 2); - border-radius: 0 var(--half-border-radius) var(--half-border-radius) 0; - background: var(--color-progress-value); - width: calc(var(--percent) * 1%); - min-width: 1px; - flex: 1; -} - -.progress-value:first-child { - border-top-left-radius: var(--half-border-radius); -} - -.progress-value:last-child { - border-bottom-left-radius: var(--half-border-radius); -} - -.progress-value-notice { - background: linear-gradient(to right, var(--color-progress-value) 65%, var(--color-negative)); -} - -.value-separator { - min-width: 2rem; - margin-inline: 0.8rem; - flex: 1; - height: calc(1em * 1.1); - border-bottom: 1px dotted var(--color-text-subdue); -} - -@container widget (min-width: 650px) { - .server { - gap: 2rem; - flex-direction: row; - align-items: center; - } - - .server + .server { - margin-top: 1rem; - } - - .server-info { - flex-direction: row-reverse; - justify-content: unset; - margin-right: auto; - z-index: 1; - } - - .server-stats { - flex-direction: row; - justify-content: right; - min-width: 450px; - margin-top: 0; - gap: 2rem; - padding-bottom: 0.8rem; - z-index: 1; - } - - .server-stats > * { - max-width: 200px; - } -} - .thumbnail { filter: grayscale(0.2) contrast(0.9); opacity: 0.8; @@ -1766,14 +1491,6 @@ details[open] .summary::after { border: 2px solid var(--color-widget-background); } -.twitch-stream-preview { - max-width: 100%; - width: 400px; - aspect-ratio: 16 / 9; - border-radius: var(--border-radius); - object-fit: cover; -} - .reddit-card-thumbnail { width: 100%; height: 100%; @@ -1839,7 +1556,7 @@ details[open] .summary::after { transform: translateY(calc(100% - var(--mobile-navigation-height))); left: var(--content-bounds-padding); right: var(--content-bounds-padding); - z-index: 11; + z-index: 10; background-color: var(--color-widget-background); border: 1px solid var(--color-widget-content-border); border-bottom: 0; @@ -1938,11 +1655,6 @@ details[open] .summary::after { .weather-column-rain::before { background-size: 7px 7px; } - - .ios .search-input { - /* so that iOS Safari does not zoom the page when the input is focused */ - font-size: 16px; - } } @media (max-width: 1190px) and (display-mode: standalone) { @@ -1950,11 +1662,7 @@ details[open] .summary::after { --safe-area-inset-bottom: env(safe-area-inset-bottom, 0); } - .ios .body-content { - height: 100dvh; - } - - .expand-toggle-button.container-expanded { + .list-collapsible-label:has(.list-collapsible-input:checked) { bottom: calc(var(--mobile-navigation-height) + var(--safe-area-inset-bottom)); } @@ -1968,10 +1676,6 @@ details[open] .summary::after { transition: padding-bottom .3s; } - .mobile-navigation-offset { - height: calc(var(--mobile-navigation-height) + var(--safe-area-inset-bottom)); - } - .mobile-navigation-icons:has(.mobile-navigation-page-links-input:checked) { padding-bottom: 0; } @@ -1985,7 +1689,7 @@ details[open] .summary::after { @media (max-width: 550px) { :root { - font-size: 9.4px; + font-size: 9px; --widget-gap: 15px; --widget-content-vertical-padding: 10px; --widget-content-horizontal-padding: 10px; @@ -2030,7 +1734,6 @@ details[open] .summary::after { .size-h6 { font-size: var(--font-size-h6); } .color-highlight { color: var(--color-text-highlight); } -.color-paragraph { color: var(--color-text-paragraph); } .color-base { color: var(--color-text-base); } .color-subdue { color: var(--color-text-subdue); } .color-negative { color: var(--color-negative); } @@ -2038,25 +1741,23 @@ details[open] .summary::after { .color-primary { color: var(--color-primary); } .cursor-help { cursor: help; } -.rounded { border-radius: var(--border-radius); } .break-all { word-break: break-all; } .text-left { text-align: left; } .text-right { text-align: right; } .text-center { text-align: center; } .text-elevate { margin-top: -0.2em; } .text-compact { word-spacing: -0.18em; } -.text-very-compact { word-spacing: -0.35em; } .rtl { direction: rtl; } .shrink { flex-shrink: 1; } .shrink-0 { flex-shrink: 0; } .min-width-0 { min-width: 0; } .max-width-100 { max-width: 100%; } +.height-100 { height: 100%; } .block { display: block; } .inline-block { display: inline-block; } .overflow-hidden { overflow: hidden; } .relative { position: relative; } .flex { display: flex; } -.flex-1 { flex: 1; } .flex-wrap { flex-wrap: wrap; } .flex-nowrap { flex-wrap: nowrap; } .justify-between { justify-content: space-between; } @@ -2069,18 +1770,15 @@ details[open] .summary::after { .flex-column { flex-direction: column; } .items-center { align-items: center; } .items-start { align-items: start; } -.items-end { align-items: end; } .gap-5 { gap: 0.5rem; } .gap-7 { gap: 0.7rem; } .gap-10 { gap: 1rem; } -.gap-12 { gap: 1.2rem; } .gap-15 { gap: 1.5rem; } .gap-20 { gap: 2rem; } .gap-25 { gap: 2.5rem; } .gap-35 { gap: 3.5rem; } .gap-45 { gap: 4.5rem; } .gap-55 { gap: 5.5rem; } -.margin-left-auto { margin-left: auto; } .margin-top-3 { margin-top: 0.3rem; } .margin-top-5 { margin-top: 0.5rem; } .margin-top-7 { margin-top: 0.7rem; } @@ -2094,7 +1792,6 @@ details[open] .summary::after { .margin-block-3 { margin-block: 0.3rem; } .margin-block-5 { margin-block: 0.5rem; } .margin-block-7 { margin-block: 0.7rem; } -.margin-block-8 { margin-block: 0.8rem; } .margin-block-10 { margin-block: 1rem; } .margin-block-15 { margin-block: 1.5rem; } .margin-bottom-3 { margin-bottom: 0.3rem; } @@ -2108,7 +1805,6 @@ details[open] .summary::after { .list { --list-half-gap: 0rem; } .list-gap-2 { --list-half-gap: 0.1rem; } .list-gap-4 { --list-half-gap: 0.2rem; } -.list-gap-8 { --list-half-gap: 0.4rem; } .list-gap-10 { --list-half-gap: 0.5rem; } .list-gap-14 { --list-half-gap: 0.7rem; } .list-gap-20 { --list-half-gap: 1rem; } diff --git a/internal/glance/static/manifest.json b/internal/assets/static/manifest.json similarity index 100% rename from internal/glance/static/manifest.json rename to internal/assets/static/manifest.json diff --git a/internal/assets/templates.go b/internal/assets/templates.go new file mode 100644 index 0000000..85abb69 --- /dev/null +++ b/internal/assets/templates.go @@ -0,0 +1,109 @@ +package assets + +import ( + "fmt" + "html/template" + "math" + "strconv" + "time" + + "golang.org/x/text/language" + "golang.org/x/text/message" +) + +var ( + PageTemplate = compileTemplate("page.html", "document.html", "page-style-overrides.gotmpl") + PageContentTemplate = compileTemplate("content.html") + CalendarTemplate = compileTemplate("calendar.html", "widget-base.html") + ClockTemplate = compileTemplate("clock.html", "widget-base.html") + BookmarksTemplate = compileTemplate("bookmarks.html", "widget-base.html") + IFrameTemplate = compileTemplate("iframe.html", "widget-base.html") + WeatherTemplate = compileTemplate("weather.html", "widget-base.html") + ForumPostsTemplate = compileTemplate("forum-posts.html", "widget-base.html") + RedditCardsHorizontalTemplate = compileTemplate("reddit-horizontal-cards.html", "widget-base.html") + RedditCardsVerticalTemplate = compileTemplate("reddit-vertical-cards.html", "widget-base.html") + ReleasesTemplate = compileTemplate("releases.html", "widget-base.html") + ChangeDetectionTemplate = compileTemplate("change-detection.html", "widget-base.html") + VideosTemplate = compileTemplate("videos.html", "widget-base.html", "video-card-contents.html") + VideosGridTemplate = compileTemplate("videos-grid.html", "widget-base.html", "video-card-contents.html") + MarketsTemplate = compileTemplate("markets.html", "widget-base.html") + RSSListTemplate = compileTemplate("rss-list.html", "widget-base.html") + RSSDetailedListTemplate = compileTemplate("rss-detailed-list.html", "widget-base.html") + RSSHorizontalCardsTemplate = compileTemplate("rss-horizontal-cards.html", "widget-base.html") + RSSHorizontalCards2Template = compileTemplate("rss-horizontal-cards-2.html", "widget-base.html") + MonitorTemplate = compileTemplate("monitor.html", "widget-base.html") + TwitchGamesListTemplate = compileTemplate("twitch-games-list.html", "widget-base.html") + TwitchChannelsTemplate = compileTemplate("twitch-channels.html", "widget-base.html") + RepositoryTemplate = compileTemplate("repository.html", "widget-base.html") + SearchTemplate = compileTemplate("search.html", "widget-base.html") + ExtensionTemplate = compileTemplate("extension.html", "widget-base.html") + GroupTemplate = compileTemplate("group.html", "widget-base.html") + DNSStatsTemplate = compileTemplate("dns-stats.html", "widget-base.html") +) + +var globalTemplateFunctions = template.FuncMap{ + "relativeTime": relativeTimeSince, + "formatViewerCount": formatViewerCount, + "formatNumber": intl.Sprint, + "absInt": func(i int) int { + return int(math.Abs(float64(i))) + }, + "formatPrice": func(price float64) string { + return intl.Sprintf("%.2f", price) + }, + "dynamicRelativeTimeAttrs": func(t time.Time) template.HTMLAttr { + return template.HTMLAttr(fmt.Sprintf(`data-dynamic-relative-time="%d"`, t.Unix())) + }, +} + +func compileTemplate(primary string, dependencies ...string) *template.Template { + t, err := template.New(primary). + Funcs(globalTemplateFunctions). + ParseFS(TemplateFS, append([]string{primary}, dependencies...)...) + + if err != nil { + panic(err) + } + + return t +} + +var intl = message.NewPrinter(language.English) + +func formatViewerCount(count int) string { + if count < 1_000 { + return strconv.Itoa(count) + } + + if count < 10_000 { + return fmt.Sprintf("%.1fk", float64(count)/1_000) + } + + if count < 1_000_000 { + return fmt.Sprintf("%dk", count/1_000) + } + + return fmt.Sprintf("%.1fm", float64(count)/1_000_000) +} + +func relativeTimeSince(t time.Time) string { + delta := time.Since(t) + + if delta < time.Minute { + return "1m" + } + if delta < time.Hour { + return fmt.Sprintf("%dm", delta/time.Minute) + } + if delta < 24*time.Hour { + return fmt.Sprintf("%dh", delta/time.Hour) + } + if delta < 30*24*time.Hour { + return fmt.Sprintf("%dd", delta/(24*time.Hour)) + } + if delta < 12*30*24*time.Hour { + return fmt.Sprintf("%dmo", delta/(30*24*time.Hour)) + } + + return fmt.Sprintf("%dy", delta/(365*24*time.Hour)) +} diff --git a/internal/glance/templates/bookmarks.html b/internal/assets/templates/bookmarks.html similarity index 58% rename from internal/glance/templates/bookmarks.html rename to internal/assets/templates/bookmarks.html index f247bdb..a4e2c97 100644 --- a/internal/glance/templates/bookmarks.html +++ b/internal/assets/templates/bookmarks.html @@ -3,17 +3,17 @@ {{ define "widget-content" }}