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 +948,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 +1065,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 +1079,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 +1095,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 +1107,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,15 +1117,6 @@ 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. @@ -1662,7 +1159,7 @@ repositories: - 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 +1168,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,17 +1176,6 @@ 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`. @@ -1704,7 +1190,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/codeberg.svg b/internal/assets/static/icons/codeberg.svg similarity index 100% rename from internal/glance/static/icons/codeberg.svg rename to internal/assets/static/icons/codeberg.svg 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 81% rename from internal/glance/static/js/main.js rename to internal/assets/static/js/main.js index 41d2ae3..ffa7eb7 100644 --- a/internal/glance/static/js/main.js +++ b/internal/assets/static/js/main.js @@ -1,6 +1,5 @@ import { setupPopovers } from './popover.js'; -import { setupMasonries } from './masonry.js'; -import { throttledDebounce, isElementVisible, openURLInNewTab } from './utils.js'; +import { throttledDebounce, isElementVisible } from './utils.js'; async function fetchPageContent(pageData) { // TODO: handle non 200 status codes/time outs @@ -49,35 +48,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 +83,7 @@ function updateRelativeTimeForElements(elements) if (timestamp === undefined) continue - element.textContent = timestampToRelativeTime(timestamp); + element.textContent = relativeTimeSince(timestamp); } } @@ -111,7 +104,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 +140,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 +245,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 +264,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"); }); } } @@ -443,7 +407,7 @@ function setupCollapsibleGrids() { let cardsPerRow; - const resolveCollapsibleItems = () => requestAnimationFrame(() => { + const resolveCollapsibleItems = () => { const hideItemsAfterIndex = cardsPerRow * collapseAfterRows; if (hideItemsAfterIndex >= gridElement.children.length) { @@ -469,7 +433,7 @@ function setupCollapsibleGrids() { child.style.removeProperty("animation-delay"); } } - }); + }; const observer = new ResizeObserver(() => { if (!isElementVisible(gridElement)) { @@ -538,34 +502,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 +547,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 +566,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 +576,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/js/utils.js b/internal/assets/static/js/utils.js similarity index 59% rename from internal/glance/static/js/utils.js rename to internal/assets/static/js/utils.js index 1d1816a..af02086 100644 --- a/internal/glance/static/js/utils.js +++ b/internal/assets/static/js/utils.js @@ -23,16 +23,3 @@ export function throttledDebounce(callback, maxDebounceTimes, debounceDelay) { export function isElementVisible(element) { return !!(element.offsetWidth || element.offsetHeight || element.getClientRects().length); } - -export function clamp(value, min, max) { - return Math.min(Math.max(value, min), max); -} - -// NOTE: inconsistent behavior between browsers when it comes to -// whether the newly opened tab gets focused or not, potentially -// depending on the event that this function is called from -export function openURLInNewTab(url, focus = true) { - const newWindow = window.open(url, '_blank', 'noopener,noreferrer'); - - if (focus && newWindow != null) newWindow.focus(); -} 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..d5ab9bb 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; } @@ -973,11 +922,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 +1013,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 +1032,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); } @@ -1107,82 +1059,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 +1095,7 @@ details[open] .summary::after { .dns-stats-graph-gridlines-container { position: absolute; + z-index: -1; inset: 0; } @@ -1228,6 +1122,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 +1168,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 +1264,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 +1339,6 @@ details[open] .summary::after { transform: translate(-50%, -50%); } -.clock-time { - min-width: 8ch; -} - .clock-time span { color: var(--color-text-highlight); } @@ -1464,7 +1355,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 +1363,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 +1374,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 +1493,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 +1558,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 +1657,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 +1664,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 +1678,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 +1691,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 +1736,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 +1743,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 +1772,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 +1794,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 +1807,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" }}