├── .dockerignore ├── vignettes ├── .gitignore ├── vscode_debug.Rmd ├── github_backend.Rmd ├── tidying_calendar.Rmd └── streamer_info.Rmd ├── LICENSE ├── .env.example ├── renv ├── .gitignore ├── settings.dcf └── activate.R ├── data └── streamer_data.rda ├── inst ├── app │ └── www │ │ ├── favicon.ico │ │ ├── timezone_detect.js │ │ └── intro.md └── golem-config.yml ├── R ├── _disable_autoload.R ├── utils-pipe.R ├── run_app.R ├── app_server.R ├── mod_feedback_fct_helpers.R ├── golem_utils_server.R ├── app_config.R ├── app_ui.R ├── mod_cal_entry.R ├── mod_feedback.R ├── golem_utils_ui.R ├── mod_cal_viewer_fct_helpers.R └── mod_cal_viewer.R ├── prototyping └── README.md ├── .gitignore ├── dev ├── app_manifest.txt ├── run_dev.R ├── python_code │ └── hello.py ├── 03_deploy.R ├── hello.R ├── r_code │ ├── hello.R │ ├── wimpys-world-of-streamers-2021-06-08.ics │ └── tmp.ics ├── 01_start.R └── 02_dev.R ├── data-raw ├── yaml_files │ ├── rctatman.yml │ ├── favstats.yml │ ├── kkent999.yml │ ├── itsmetoeknee.yml │ ├── wviechtb.yml │ ├── kierisi.yml │ ├── mcmullarkey.yml │ ├── statsinthewild.yml │ ├── tanho_.yml │ ├── moriah_streamR.yml │ ├── aklongmuir.yml │ ├── lisadebruine.yml │ ├── nickwan_datasci.yml │ ├── rpodcast.yml │ ├── UCeiiqmVK07qhY-wvg3IZiZQ.yml │ ├── waylonwalker.yml │ ├── thecedarprince.yml │ ├── theeatgamelove.yml │ └── aftonsteps.yml └── streamer_data.R ├── .Rbuildignore ├── .Renviron.example ├── .lintr ├── .devcontainer ├── rstudio_docker │ ├── .env │ ├── rstudio.env │ └── Dockerfile ├── launch.json ├── .env ├── library-scripts │ ├── r-packages-setup.sh │ ├── .Rprofile-vscode │ └── common-debian.sh ├── keybindings.json ├── docker-compose.yml ├── rstudio_config_dir │ └── rstudio-prefs.json ├── devcontainer.json └── Dockerfile ├── app.R ├── shinycal.Rproj ├── man ├── import_cal.Rd ├── pipe.Rd └── run_app.Rd ├── NAMESPACE ├── LICENSE.md ├── DESCRIPTION ├── README.md ├── .github └── CONTRIBUTING.md ├── README.Rmd ├── NEWS.md ├── testapp └── app.R ├── .Rprofile ├── CODE_OF_CONDUCT.md └── renv.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | .git/ 2 | -------------------------------------------------------------------------------- /vignettes/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | *.R 3 | .Renviron -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | YEAR: 2021 2 | COPYRIGHT HOLDER: Eric Nantz 3 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | NEXTCLOUD_USER="username" 2 | NEXTCLOUD_PASSWORD="mypassword" -------------------------------------------------------------------------------- /renv/.gitignore: -------------------------------------------------------------------------------- 1 | library/ 2 | local/ 3 | lock/ 4 | python/ 5 | staging/ 6 | -------------------------------------------------------------------------------- /data/streamer_data.rda: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rpodcast/shinycal/HEAD/data/streamer_data.rda -------------------------------------------------------------------------------- /inst/app/www/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rpodcast/shinycal/HEAD/inst/app/www/favicon.ico -------------------------------------------------------------------------------- /R/_disable_autoload.R: -------------------------------------------------------------------------------- 1 | # Disabling shiny autoload 2 | 3 | # See ?shiny::loadSupport for more information 4 | -------------------------------------------------------------------------------- /prototyping/README.md: -------------------------------------------------------------------------------- 1 | Use this directory for exploring quick things. By default the contents of this directory will not be version controlled! -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .Renviron 2 | .Rhistory 3 | .Rproj.user 4 | .env 5 | inst/doc 6 | prototyping/* 7 | !prototyping/README.md 8 | README.html 9 | node_modules/* -------------------------------------------------------------------------------- /dev/app_manifest.txt: -------------------------------------------------------------------------------- 1 | data 2 | data-raw 3 | inst 4 | man 5 | R 6 | app.R 7 | DESCRIPTION 8 | NAMESPACE 9 | NEWS.md 10 | README.md 11 | .Renviron -------------------------------------------------------------------------------- /data-raw/yaml_files/rctatman.yml: -------------------------------------------------------------------------------- 1 | user_id: rctatman 2 | name: Rachael Tatman 3 | twitter_id: rctatman 4 | platform: twitch 5 | categories: 6 | - datascience -------------------------------------------------------------------------------- /data-raw/yaml_files/favstats.yml: -------------------------------------------------------------------------------- 1 | user_id: favstats 2 | name: Fabio Votta 3 | twitter_id: favstats 4 | platform: twitch 5 | categories: 6 | - datascience 7 | - r -------------------------------------------------------------------------------- /data-raw/yaml_files/kkent999.yml: -------------------------------------------------------------------------------- 1 | user_id: kkent999 2 | name: Kevin Kent 3 | twitter_id: kevin_m_kent 4 | platform: twitch 5 | categories: 6 | - datascience 7 | - r -------------------------------------------------------------------------------- /data-raw/yaml_files/itsmetoeknee.yml: -------------------------------------------------------------------------------- 1 | user_id: itsmetoeknee 2 | name: Tony Elhabr 3 | twitter_id: TonyElHabr 4 | platform: twitch 5 | categories: 6 | - datascience 7 | - r -------------------------------------------------------------------------------- /data-raw/yaml_files/wviechtb.yml: -------------------------------------------------------------------------------- 1 | user_id: wviechtb 2 | name: Wolfgang Viechtbauer 3 | twitter_id: wviechtb 4 | platform: twitch 5 | categories: 6 | - datascience 7 | - r -------------------------------------------------------------------------------- /data-raw/yaml_files/kierisi.yml: -------------------------------------------------------------------------------- 1 | user_id: kierisi 2 | name: Jesse Mostipak 3 | twitter_id: kierisi 4 | platform: twitch 5 | categories: 6 | - r 7 | - datascience 8 | - gaming -------------------------------------------------------------------------------- /data-raw/yaml_files/mcmullarkey.yml: -------------------------------------------------------------------------------- 1 | user_id: mcmullarkey 2 | name: Michael Mullarkey 3 | twitter_id: mcmullarkey 4 | platform: twitch 5 | categories: 6 | - datascience 7 | - r -------------------------------------------------------------------------------- /data-raw/yaml_files/statsinthewild.yml: -------------------------------------------------------------------------------- 1 | user_id: statsinthewild 2 | name: Greg Matthews 3 | twitter_id: statsinthewild 4 | platform: twitch 5 | categories: 6 | - datascience 7 | - r -------------------------------------------------------------------------------- /data-raw/yaml_files/tanho_.yml: -------------------------------------------------------------------------------- 1 | user_id: tanho_ 2 | name: Tan Ho 3 | twitter_id: _TanHo 4 | platform: twitch 5 | categories: 6 | - r 7 | - datascience 8 | - shiny 9 | - sports -------------------------------------------------------------------------------- /data-raw/yaml_files/moriah_streamR.yml: -------------------------------------------------------------------------------- 1 | user_id: moriah_streamR 2 | name: Moriah Taylor 3 | twitter_id: moriah_taylor58 4 | platform: twitch 5 | categories: 6 | - r 7 | - datascience -------------------------------------------------------------------------------- /data-raw/yaml_files/aklongmuir.yml: -------------------------------------------------------------------------------- 1 | user_id: aklongmuir 2 | name: Alyssa Longmuir 3 | twitter_id: alyssastweeting 4 | platform: twitch 5 | categories: 6 | - r 7 | - datascience 8 | - sports -------------------------------------------------------------------------------- /data-raw/yaml_files/lisadebruine.yml: -------------------------------------------------------------------------------- 1 | user_id: lisadebruine 2 | name: Lisa DeBruine 3 | twitter_id: lisadebruine 4 | platform: twitch 5 | categories: 6 | - r 7 | - datascience 8 | - shiny -------------------------------------------------------------------------------- /data-raw/yaml_files/nickwan_datasci.yml: -------------------------------------------------------------------------------- 1 | user_id: nickwan_datasci 2 | name: Nick Wan 3 | twitter_id: nickwan 4 | platform: twitch 5 | categories: 6 | - python 7 | - datascience 8 | - gaming -------------------------------------------------------------------------------- /data-raw/yaml_files/rpodcast.yml: -------------------------------------------------------------------------------- 1 | user_id: rpodcast 2 | name: Eric Nantz 3 | twitter_id: thercast 4 | platform: twitch 5 | categories: 6 | - r 7 | - datascience 8 | - shiny 9 | - linux -------------------------------------------------------------------------------- /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^renv$ 2 | ^renv\.lock$ 3 | ^LICENSE\.md$ 4 | ^.*\.Rproj$ 5 | ^\.Rproj\.user$ 6 | ^README\.Rmd$ 7 | ^CODE_OF_CONDUCT\.md$ 8 | ^app\.R$ 9 | ^rsconnect$ 10 | ^data-raw$ 11 | -------------------------------------------------------------------------------- /.Renviron.example: -------------------------------------------------------------------------------- 1 | NEXTCLOUD_USER="username" 2 | NEXTCLOUD_PASSWORD="mypassword" 3 | TWITCH_CLIENT_ID=aaaaaaaaaaa 4 | TWITCH_SECRET=bbbbbbbbbbbbbbbbbbb 5 | YOUTUBE_API_KEY=hhhhhhhhhhhhhhhhhhhhhhhhh -------------------------------------------------------------------------------- /data-raw/yaml_files/UCeiiqmVK07qhY-wvg3IZiZQ.yml: -------------------------------------------------------------------------------- 1 | user_id: UCeiiqmVK07qhY-wvg3IZiZQ 2 | name: David Robinson 3 | twitter_id: drob 4 | platform: youtube 5 | categories: 6 | - r 7 | - datascience -------------------------------------------------------------------------------- /data-raw/yaml_files/waylonwalker.yml: -------------------------------------------------------------------------------- 1 | user_id: waylonwalker 2 | name: Waylon Walker 3 | twitter_id: waylonwalker 4 | platform: twitch 5 | categories: 6 | - datascience 7 | - python 8 | - linux -------------------------------------------------------------------------------- /data-raw/yaml_files/thecedarprince.yml: -------------------------------------------------------------------------------- 1 | user_id: thecedarprince 2 | name: Jacob Zelko 3 | twitter_id: jacob_zelko 4 | platform: twitch 5 | categories: 6 | - datascience 7 | - r 8 | - python 9 | - julia -------------------------------------------------------------------------------- /data-raw/yaml_files/theeatgamelove.yml: -------------------------------------------------------------------------------- 1 | user_id: theeatgamelove 2 | name: Kyle Harris and Alexis Meskowski 3 | twitter_id: theeatgamelove 4 | platform: twitch 5 | categories: 6 | - r 7 | - datascience 8 | - gaming -------------------------------------------------------------------------------- /.lintr: -------------------------------------------------------------------------------- 1 | linters: with_defaults( 2 | line_length_linter(120), 3 | commented_code_linter = NULL, 4 | object_usage_linter = NULL, 5 | trailing_blank_lines_linter = NULL, 6 | trailing_whitespace_linter = NULL 7 | ) 8 | -------------------------------------------------------------------------------- /.devcontainer/rstudio_docker/.env: -------------------------------------------------------------------------------- 1 | RENV_PATHS_CACHE_HOST="/opt/local/renv/cache_rstudio" 2 | RENV_PATHS_CACHE_CONTAINER="/renv/cache" 3 | RENV_PATHS_CACHE="/renv/cache" 4 | USER="eric" 5 | USERID=1000 6 | GROUPID=1000 7 | PASSWORD=1rstudio -------------------------------------------------------------------------------- /.devcontainer/rstudio_docker/rstudio.env: -------------------------------------------------------------------------------- 1 | RENV_PATHS_CACHE_HOST=/opt/local/renv/cache_rstudio 2 | RENV_PATHS_CACHE_CONTAINER=/renv/cache 3 | RENV_PATHS_CACHE=/renv/cache 4 | USER=eric 5 | USERID=1000 6 | GROUPID=1000 7 | PASSWORD=1rstudio -------------------------------------------------------------------------------- /data-raw/yaml_files/aftonsteps.yml: -------------------------------------------------------------------------------- 1 | user_id: aftonsteps 2 | name: Afton Coombs 3 | twitter_id: aftonsteps 4 | platform: twitch 5 | bgColor: "#008080" 6 | color: "white" 7 | categories: 8 | - r 9 | - datascience 10 | - shiny -------------------------------------------------------------------------------- /renv/settings.dcf: -------------------------------------------------------------------------------- 1 | external.libraries: 2 | ignored.packages: 3 | package.dependency.fields: Imports, Depends, LinkingTo 4 | r.version: 5 | snapshot.type: implicit 6 | use.cache: TRUE 7 | vcs.ignore.library: TRUE 8 | vcs.ignore.local: TRUE 9 | -------------------------------------------------------------------------------- /.devcontainer/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "R-Debugger", 3 | "request": "launch", 4 | "name": "Debug Shiny App", 5 | "debugMode": "file", 6 | "workingDirectory": "${workspaceFolder}", 7 | "file": "${workspaceFolder}/dev/run_dev.R", 8 | "allowGlobalDebugging": false 9 | } 10 | -------------------------------------------------------------------------------- /.devcontainer/.env: -------------------------------------------------------------------------------- 1 | 2 | RENV_PATHS_CACHE_HOST=/opt/local/renv/cache 3 | RENV_PATHS_CACHE_CONTAINER=/renv/cache 4 | RENV_PATHS_CACHE=/renv/cache 5 | USER=eric 6 | USERID=1000 7 | GROUPID=1000 8 | PASSWORD=1rstudio 9 | VOLUME_PATH=/home/eric/shinydevseries_projects 10 | LOCAL_PORT=7111 11 | CONTAINER_NAME=shinycal 12 | -------------------------------------------------------------------------------- /app.R: -------------------------------------------------------------------------------- 1 | # Launch the ShinyApp (Do not remove this comment) 2 | # To deploy, run: rsconnect::deployApp() 3 | # Or use the blue button on top of this file 4 | 5 | pkgload::load_all(export_all = FALSE,helpers = FALSE,attach_testthat = FALSE) 6 | options( "golem.app.prod" = TRUE) 7 | shinycal::run_app() # add parameters here (if any) 8 | -------------------------------------------------------------------------------- /dev/run_dev.R: -------------------------------------------------------------------------------- 1 | # Set options here 2 | options(golem.app.prod = FALSE) # TRUE = production mode, FALSE = development mode 3 | 4 | # Detach all loaded packages and clean your environment 5 | golem::detach_all_attached() 6 | # rm(list=ls(all.names = TRUE)) 7 | 8 | # Document and reload your package 9 | golem::document_and_reload() 10 | 11 | # Run the application 12 | run_app() 13 | -------------------------------------------------------------------------------- /shinycal.Rproj: -------------------------------------------------------------------------------- 1 | Version: 1.0 2 | 3 | RestoreWorkspace: Default 4 | SaveWorkspace: Default 5 | AlwaysSaveHistory: Default 6 | 7 | EnableCodeIndexing: Yes 8 | UseSpacesForTab: Yes 9 | NumSpacesForTab: 2 10 | Encoding: UTF-8 11 | 12 | RnwWeave: Sweave 13 | LaTeX: pdfLaTeX 14 | 15 | BuildType: Package 16 | PackageUseDevtools: Yes 17 | PackageInstallArgs: --no-multiarch --with-keep.source 18 | -------------------------------------------------------------------------------- /R/utils-pipe.R: -------------------------------------------------------------------------------- 1 | #' Pipe operator 2 | #' 3 | #' See \code{magrittr::\link[magrittr:pipe]{\%>\%}} for details. 4 | #' 5 | #' @name %>% 6 | #' @rdname pipe 7 | #' @keywords internal 8 | #' @export 9 | #' @importFrom magrittr %>% 10 | #' @usage lhs \%>\% rhs 11 | #' @param lhs A value or the magrittr placeholder. 12 | #' @param rhs A function call using the magrittr semantics. 13 | #' @return The result of calling `rhs(lhs)`. 14 | NULL 15 | -------------------------------------------------------------------------------- /inst/golem-config.yml: -------------------------------------------------------------------------------- 1 | default: 2 | golem_name: shinycal 3 | golem_version: 0.8 4 | app_prod: no 5 | cal_host: "nextcloud.r-podcastdev.link" 6 | cal_base_url: "https://nextcloud.r-podcastdev.link/remote.php/dav/calendars/shinycal" 7 | streamer_data_repo: "git@github.com:rpodcast/streamer_data_dev.git" 8 | production: 9 | app_prod: yes 10 | streamer_data_repo: "git@github.com:rpodcast/streamer_data.git" 11 | dev: 12 | golem_wd: !expr here::here() 13 | -------------------------------------------------------------------------------- /.devcontainer/library-scripts/r-packages-setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # install depedencies for httpgd 4 | apt-get -y install --no-install-recommends libfreetype6-dev libfontconfig1-dev 5 | 6 | # Use littler to install packages 7 | install2.r languageserver renv remotes 8 | installGithub.r nx10/httpgd 9 | 10 | # establish renv environment variables in users .Renviron file 11 | mkdir -p /renv/cache 12 | echo "RENV_PATHS_CACHE=/renv/cache" >> /usr/local/lib/R/etc/Renviron 13 | -------------------------------------------------------------------------------- /inst/app/www/timezone_detect.js: -------------------------------------------------------------------------------- 1 | $( document ).ready(function() { 2 | var d = new Date(); 3 | var target = $('#clientTime'); 4 | var offset = $('#clientTimeOffset'); 5 | var timezone = $('#clientZone') 6 | 7 | offset.val(d.getTimezoneOffset()); 8 | offset.trigger("change"); 9 | target.val(d.toLocaleString()); 10 | target.trigger("change"); 11 | 12 | timezone.val(Intl.DateTimeFormat().resolvedOptions().timeZone); 13 | timezone.trigger("change") 14 | }); 15 | -------------------------------------------------------------------------------- /.devcontainer/keybindings.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "key": "ctrl+v", 4 | "command": "pasteAndIndent.action", 5 | "when": "editorTextFocus && !editorReadonly" 6 | }, 7 | { 8 | "key": "ctrl+v", 9 | "command": "editor.action.clipboardPasteAction", 10 | "when": "!editorTextFocus" 11 | }, 12 | { 13 | "key": "ctrl+shift+v", 14 | "command": "editor.action.clipboardPasteAction", 15 | "when": "editorTextFocus && !editorReadonly" 16 | } 17 | ] -------------------------------------------------------------------------------- /man/import_cal.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/mod_cal_viewer_fct_helpers.R 3 | \name{import_cal} 4 | \alias{import_cal} 5 | \title{Import calendar directly from server} 6 | \usage{ 7 | import_cal(cal_slug = "wimpys-world-of-streamers", cal_base_url = NULL) 8 | } 9 | \arguments{ 10 | \item{cal_slug}{string for URL slug of calendar.} 11 | } 12 | \value{ 13 | data frame with calendar event contents 14 | } 15 | \description{ 16 | Import calendar directly from server 17 | } 18 | -------------------------------------------------------------------------------- /man/pipe.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/utils-pipe.R 3 | \name{\%>\%} 4 | \alias{\%>\%} 5 | \title{Pipe operator} 6 | \usage{ 7 | lhs \%>\% rhs 8 | } 9 | \arguments{ 10 | \item{lhs}{A value or the magrittr placeholder.} 11 | 12 | \item{rhs}{A function call using the magrittr semantics.} 13 | } 14 | \value{ 15 | The result of calling `rhs(lhs)`. 16 | } 17 | \description{ 18 | See \code{magrittr::\link[magrittr:pipe]{\%>\%}} for details. 19 | } 20 | \keyword{internal} 21 | -------------------------------------------------------------------------------- /inst/app/www/intro.md: -------------------------------------------------------------------------------- 1 | In the last few years, we have seen an huge uptick in the number of data science professionals and hobbyists who like to stream their projects and share their knowledge with the community! This calendar is my small attempt to contribute back to this awesome community so you can see what great streams are happening! 2 | 3 | Check out Jesse Mostipak's [Data Science Streamers](https://www.jessemaegan.com/blog/2021-05-28-data-science-twitch-streamers-round-up/) blog post for a great roundup of the content creators seen on this calendar! -------------------------------------------------------------------------------- /.devcontainer/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | shinycal: 5 | build: 6 | context: ./rstudio_docker 7 | container_name: ${CONTAINER_NAME} 8 | volumes: 9 | - ${VOLUME_PATH}:${VOLUME_PATH} 10 | - /home/${USER}/.ssh:/home/${USER}/.ssh 11 | - ${RENV_PATHS_CACHE_HOST}:${RENV_PATHS_CACHE_CONTAINER} 12 | - ${VOLUME_PATH}/.devcontainer/rstudio_config_dir:/home/${USER}/.config/rstudio 13 | restart: 14 | unless-stopped 15 | ports: 16 | - ${LOCAL_PORT}:8787 17 | env_file: .env 18 | 19 | -------------------------------------------------------------------------------- /R/run_app.R: -------------------------------------------------------------------------------- 1 | #' Run the Shiny Application 2 | #' 3 | #' @param ... arguments to pass to golem_opts. 4 | #' See `?golem::get_golem_options` for more details. 5 | #' @inheritParams shiny::shinyApp 6 | #' 7 | #' @export 8 | #' @importFrom shiny shinyApp 9 | #' @importFrom golem with_golem_options 10 | run_app <- function( 11 | onStart = NULL, 12 | options = list(), 13 | enableBookmarking = NULL, 14 | uiPattern = "/", 15 | ... 16 | ) { 17 | with_golem_options( 18 | app = shinyApp( 19 | ui = app_ui, 20 | server = app_server, 21 | onStart = onStart, 22 | options = options, 23 | enableBookmarking = enableBookmarking, 24 | uiPattern = uiPattern 25 | ), 26 | golem_opts = list(...) 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /vignettes/vscode_debug.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "vscode debug" 3 | output: html_document 4 | --- 5 | 6 | ## Obtaining metadata associated with streamers 7 | 8 | The goal of this vignette is to illustrate how we can get interesting metadata associated with each streamer via the Twitch and YouTube APIs. 9 | 10 | ## Setup 11 | 12 | ```{r setup, include = FALSE} 13 | library(twitchr) 14 | library(httr) 15 | library(calendar) 16 | library(caldav) 17 | library(dplyr) 18 | library(yaml) 19 | 20 | print(getwd()) 21 | print(here::here()) 22 | readRenviron(".Renviron") 23 | ``` 24 | 25 | ## Twitch streamer metadata 26 | 27 | Hello during my stream 28 | 29 | ```{r twitchauth, eval=TRUE} 30 | # set up authentication via environment variables 31 | twitch_auth() 32 | ``` 33 | -------------------------------------------------------------------------------- /R/app_server.R: -------------------------------------------------------------------------------- 1 | #' The application server-side 2 | #' 3 | #' @param input,output,session Internal parameters for {shiny}. 4 | #' DO NOT REMOVE. 5 | #' @import shiny 6 | #' @noRd 7 | app_server <- function( input, output, session ) { 8 | 9 | # obtain custom inputs derived from javascript time change function 10 | output$server <- renderText({ c("Server time:", as.character(Sys.time()), as.character(Sys.timezone())) }) 11 | 12 | session$userData$time <- reactive({ 13 | format(lubridate::mdy_hms(as.character(input$clientTime)), "%d/%m/%Y; %H:%M:%S") 14 | }) 15 | 16 | session$userData$offset <- reactive({ 17 | input$clientTimeOffset 18 | }) 19 | 20 | session$userData$zone <- reactive({ 21 | input$clientZone 22 | }) 23 | 24 | # specific to shinylogs 25 | #shinylogs::track_usage(storage_mode = store_null()) 26 | 27 | # Your application server logic 28 | mod_cal_viewer_server("cal_viewer_ui_1") 29 | #mod_cal_entry_server("cal_entry_ui_1") 30 | mod_feedback_server("feedback_ui_1") 31 | } 32 | -------------------------------------------------------------------------------- /.devcontainer/rstudio_config_dir/rstudio-prefs.json: -------------------------------------------------------------------------------- 1 | { 2 | "panes": { 3 | "quadrants": [ 4 | "Source", 5 | "TabSet1", 6 | "Console", 7 | "TabSet2" 8 | ], 9 | "tabSet1": [ 10 | "History", 11 | "Presentation" 12 | ], 13 | "tabSet2": [ 14 | "Environment", 15 | "Files", 16 | "Plots", 17 | "Connections", 18 | "Packages", 19 | "Help", 20 | "Build", 21 | "VCS", 22 | "Tutorial", 23 | "Viewer" 24 | ], 25 | "console_left_on_top": false, 26 | "console_right_on_top": true 27 | }, 28 | "save_workspace": "never", 29 | "load_workspace": false, 30 | "auto_append_newline": true, 31 | "font_size_points": 14, 32 | "editor_theme": "Tomorrow Night 80s", 33 | "posix_terminal_shell": "bash", 34 | "default_project_location": "~/r_dev_projects", 35 | "initial_working_directory": "~" 36 | } -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | export("%>%") 4 | export(import_cal) 5 | export(run_app) 6 | import(caldav) 7 | import(shiny) 8 | import(shinylogs) 9 | import(shinyvalidate) 10 | import(toastui) 11 | importFrom(calendar,ic_read) 12 | importFrom(calendar,ical) 13 | importFrom(clock,as_naive_time) 14 | importFrom(clock,as_zoned_time) 15 | importFrom(clock,date_time_parse) 16 | importFrom(clock,zoned_time_set_zone) 17 | importFrom(dplyr,case_when) 18 | importFrom(dplyr,filter) 19 | importFrom(dplyr,left_join) 20 | importFrom(dplyr,mutate) 21 | importFrom(dplyr,select) 22 | importFrom(golem,activate_js) 23 | importFrom(golem,add_resource_path) 24 | importFrom(golem,bundle_resources) 25 | importFrom(golem,favicon) 26 | importFrom(golem,with_golem_options) 27 | importFrom(magrittr,"%>%") 28 | importFrom(purrr,map) 29 | importFrom(purrr,map2) 30 | importFrom(shiny,HTML) 31 | importFrom(shiny,NS) 32 | importFrom(shiny,column) 33 | importFrom(shiny,shinyApp) 34 | importFrom(shiny,tagAppendAttributes) 35 | importFrom(shiny,tagList) 36 | importFrom(shiny,tags) 37 | importFrom(twitchr,get_users) 38 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2021 Eric Nantz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: shinycal 2 | Title: The data science streamRs calendar 3 | Version: 0.8 4 | Authors@R: person('Eric', 'Nantz', email = 'thercast@gmail.com', role = c('cre', 'aut')) 5 | Description: A shiny application serving an interactive calendar for 6 | data science streams on various platforms such as Twitch. 7 | License: MIT + file LICENSE 8 | Imports: 9 | calendar, 10 | config (>= 0.3.1), 11 | dplyr, 12 | golem (>= 0.3.1), 13 | magrittr, 14 | pkgload, 15 | purrr, 16 | shiny (>= 1.6.0), 17 | clock, 18 | shinylogs, 19 | caldav (>= 0.1.0), 20 | toastui (>= 0.1.2.9300), 21 | tidyr, 22 | fs, 23 | stringr, 24 | readr, 25 | httr, 26 | tibble, 27 | twitchr (>= 0.1.0), 28 | colourpicker, 29 | shinyWidgets, 30 | rvest, 31 | shinyvalidate, 32 | gh 33 | Encoding: UTF-8 34 | LazyData: true 35 | RoxygenNote: 7.1.1 36 | URL: https://github.com/rpodcast/shinycal 37 | BugReports: https://github.com/rpodcast/shinycal/issues 38 | Suggests: 39 | rmarkdown, 40 | knitr 41 | VignetteBuilder: knitr 42 | Depends: 43 | R (>= 2.10) 44 | Remotes: 45 | petermeissner/caldav, 46 | dreamRs/toastui, 47 | KoderKow/twitchr 48 | -------------------------------------------------------------------------------- /R/mod_feedback_fct_helpers.R: -------------------------------------------------------------------------------- 1 | get_issue_labels <- function(owner = "rpodcast", repo = "shinycal") { 2 | issues_raw <- gh::gh("GET /repos/{owner}/{repo}/labels", owner = "rpodcast", repo = "shinycal") 3 | 4 | labels_df <- tibble::tibble(x = issues_raw) %>% 5 | tidyr::unnest_wider(1) 6 | 7 | labels <- dplyr::pull(labels_df, name) 8 | 9 | return(labels) 10 | } 11 | 12 | submit_issue <- function( 13 | issue_title, 14 | issue_labels, 15 | issue_description, 16 | repo = "shinycal", 17 | owner = "rpodcast", 18 | add_issues = "user feedback" 19 | ) { 20 | # append name and date to end of issue description 21 | version <- golem::get_golem_version() 22 | issue_date <- Sys.Date() 23 | 24 | end_text <- glue::glue("\n\n\n\nFile from shinycal version {version} on {issue_date}") 25 | 26 | issue_description <- paste0(issue_description, end_text) 27 | 28 | # add additional labels 29 | issue_labels <- c(issue_labels, add_issues) 30 | 31 | # submit 32 | issue_post <- gh::gh( 33 | glue::glue("POST /repos/{owner}/{repo}/issues"), 34 | title = issue_title, 35 | body = issue_description, 36 | assignee = "rpodcast", 37 | labels = issue_labels 38 | ) 39 | 40 | return(issue_post) 41 | } 42 | 43 | -------------------------------------------------------------------------------- /R/golem_utils_server.R: -------------------------------------------------------------------------------- 1 | #' Inverted versions of in, is.null and is.na 2 | #' 3 | #' @noRd 4 | #' 5 | #' @examples 6 | #' 1 %not_in% 1:10 7 | #' not_null(NULL) 8 | `%not_in%` <- Negate(`%in%`) 9 | 10 | not_null <- Negate(is.null) 11 | 12 | not_na <- Negate(is.na) 13 | 14 | #' Removes the null from a vector 15 | #' 16 | #' @noRd 17 | #' 18 | #' @example 19 | #' drop_nulls(list(1, NULL, 2)) 20 | drop_nulls <- function(x){ 21 | x[!sapply(x, is.null)] 22 | } 23 | 24 | #' If x is `NULL`, return y, otherwise return x 25 | #' 26 | #' @param x,y Two elements to test, one potentially `NULL` 27 | #' 28 | #' @noRd 29 | #' 30 | #' @examples 31 | #' NULL %||% 1 32 | "%||%" <- function(x, y){ 33 | if (is.null(x)) { 34 | y 35 | } else { 36 | x 37 | } 38 | } 39 | 40 | #' If x is `NA`, return y, otherwise return x 41 | #' 42 | #' @param x,y Two elements to test, one potentially `NA` 43 | #' 44 | #' @noRd 45 | #' 46 | #' @examples 47 | #' NA %||% 1 48 | "%|NA|%" <- function(x, y){ 49 | if (is.na(x)) { 50 | y 51 | } else { 52 | x 53 | } 54 | } 55 | 56 | #' Typing reactiveValues is too long 57 | #' 58 | #' @inheritParams reactiveValues 59 | #' @inheritParams reactiveValuesToList 60 | #' 61 | #' @noRd 62 | rv <- shiny::reactiveValues 63 | rvtl <- shiny::reactiveValuesToList 64 | 65 | -------------------------------------------------------------------------------- /R/app_config.R: -------------------------------------------------------------------------------- 1 | #' Access files in the current app 2 | #' 3 | #' NOTE: If you manually change your package name in the DESCRIPTION, 4 | #' don't forget to change it here too, and in the config file. 5 | #' For a safer name change mechanism, use the `golem::set_golem_name()` function. 6 | #' 7 | #' @param ... character vectors, specifying subdirectory and file(s) 8 | #' within your package. The default, none, returns the root of the app. 9 | #' 10 | #' @noRd 11 | app_sys <- function(...){ 12 | system.file(..., package = "shinycal") 13 | } 14 | 15 | 16 | #' Read App Config 17 | #' 18 | #' @param value Value to retrieve from the config file. 19 | #' @param config GOLEM_CONFIG_ACTIVE value. If unset, R_CONFIG_ACTIVE. 20 | #' If unset, "default". 21 | #' @param use_parent Logical, scan the parent directory for config file. 22 | #' 23 | #' @noRd 24 | get_golem_config <- function( 25 | value, 26 | config = Sys.getenv( 27 | "GOLEM_CONFIG_ACTIVE", 28 | Sys.getenv( 29 | "R_CONFIG_ACTIVE", 30 | "default" 31 | ) 32 | ), 33 | use_parent = TRUE 34 | ){ 35 | config::get( 36 | value = value, 37 | config = config, 38 | # Modify this if your config file is somewhere else: 39 | file = app_sys("golem-config.yml"), 40 | use_parent = use_parent 41 | ) 42 | } 43 | 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # The Data Science StreamRs Calendar\! 5 | 6 | 7 | 8 | 9 | 10 | In the last few years, we have seen an huge uptick in the number of data 11 | science professionals and hobbyists who like to stream their projects 12 | and share their knowledge with the community\! This calendar is my small 13 | attempt to contribute back to this awesome community so you can see what 14 | great streams are happening\! 15 | 16 | This repository contains the Shiny app providing the streamers calendar, 17 | created with the [`{golem}`](https://golemverse.org/) package. In 18 | addition, this app would not be possible without the 19 | [`{toastui}`](https://github.com/dreamRs/toastui) package by the fine 20 | friends at DreamRs. 21 | 22 | ## Installation 23 | 24 | You can install the development version from 25 | [GitHub](https://github.com/) with: 26 | 27 | ``` r 28 | # install.packages("remotes") 29 | remotes::install_github("rpodcast/shinycal") 30 | ``` 31 | 32 | ## Code of Conduct 33 | 34 | Please note that the shinycal project is released with a [Contributor 35 | Code of 36 | Conduct](https://contributor-covenant.org/version/2/0/CODE_OF_CONDUCT.html). 37 | By contributing to this project, you agree to abide by its terms. 38 | -------------------------------------------------------------------------------- /data-raw/streamer_data.R: -------------------------------------------------------------------------------- 1 | ## code to prepare `streamer_data` dataset goes here 2 | devtools::load_all() 3 | # yml_file <- "data-raw/streamers.yml" 4 | # yml_data <- yaml::read_yaml(file = yml_file) 5 | 6 | yml_files <- fs::dir_ls("data-raw/yaml_files") 7 | 8 | yml_data <- purrr::map(yml_files, ~yaml::read_yaml(file = .x)) 9 | 10 | #tibble::tibble(yml_data2) %>% tidyr::unnest_wider(1) 11 | 12 | # set up authentication via environment variables 13 | twitchr::twitch_auth() 14 | 15 | streamer_data <- tibble::tibble(yml_data) %>% 16 | tidyr::unnest_wider(1) %>% 17 | dplyr::filter(platform == "twitch") %>% 18 | dplyr::mutate(user_data = purrr::map(user_id, ~get_twitch_id(user_name = .x))) %>% 19 | tidyr::unnest(cols = user_data) %>% 20 | #dplyr::mutate(id = purrr::map_chr(user_id, ~get_twitch_id(user_name = .x))) %>% 21 | dplyr::mutate(schedule_data = purrr::map(id, ~get_twitch_schedule(.x)), 22 | videos_data = purrr::map(id, ~get_twitch_videos(.x))) 23 | 24 | usethis::use_data(streamer_data, overwrite = TRUE) 25 | 26 | 27 | # streamer_data2 <- streamer_data %>% 28 | # dplyr::mutate(schedule_data = purrr::map(id, ~get_twitch_schedule(.x))) 29 | 30 | # streamer_data2 %>% 31 | # tidyr::unnest(schedule_data) %>% 32 | # mutate(calendarId = 1) %>% 33 | # mutate(id = seq_len(dplyr::n())) -------------------------------------------------------------------------------- /.devcontainer/library-scripts/.Rprofile-vscode: -------------------------------------------------------------------------------- 1 | # setup if using with vscode and R plugin 2 | if (Sys.getenv("TERM_PROGRAM") == "vscode") { 3 | source(file.path(Sys.getenv(if (.Platform$OS.type == "windows") "USERPROFILE" else "HOME"), ".vscode-R", "init.R")) 4 | } 5 | source("renv/activate.R") 6 | 7 | if (Sys.getenv("TERM_PROGRAM") == "vscode") { 8 | # obtain list of packages in renv library currently 9 | project <- renv:::renv_project_resolve(NULL) 10 | lib_packages <- names(unclass(renv:::renv_diagnostics_packages_library(project))$Packages) 11 | 12 | # detect whether key packages are already installed 13 | # was: !require("languageserver") 14 | if (!"languageserver" %in% lib_packages) { 15 | message("installing languageserver package") 16 | renv::install("languageserver") 17 | } 18 | 19 | if (!"httpgd" %in% lib_packages) { 20 | message("installing httpgd package") 21 | renv::install("nx10/httpgd") 22 | } 23 | 24 | if (!"vscDebugger" %in% lib_packages) { 25 | message("installation vscDebugger package") 26 | renv::install("ManuelHentschel/vscDebugger@v0.4.5") 27 | } 28 | 29 | # use the new httpgd plotting device 30 | options(vsc.plot = FALSE) 31 | options(device = function(...) { 32 | httpgd:::hgd() 33 | .vsc.browser(httpgd::hgd_url(), viewer = "Beside") 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to **shinycal** 2 | 3 | There are many ways to contribute to the ongoing development of the **shinycal** package! Some contributions can be rather easy to do (e.g., fixing typos, improving documentation, filing issues for feature requests or problems, etc.) whereas other contributions can require more time and patience (like answering questions and submitting pull requests with code changes). Just know that that help provided in any capacity is very much appreciated. :) 4 | 5 | ## Filing Issues 6 | 7 | If you believe you found a bug with the application, please include details on how you arrived at the bug (i.e. did you click on a certain input, choose a specific value, etc). If you found issues in the code itself, please include specific details on the location of the bug in the code base, and if possible create a reprex that demonstrates the issue. Details on creating a reprex in a Shiny app can be found in the [getting help section](https://mastering-shiny.org/action-workflow.html#getting-help) of [Mastering Shiny](https://mastering-shiny.org/index.html). 8 | 9 | ### Making Pull Requests 10 | 11 | Should you consider making a pull request (PR), please file an issue first and explain the problem or feature idea in some detail. If the PR is an enhancement, detail how the change would make things better for the general user base of the application. 12 | 13 | -------------------------------------------------------------------------------- /README.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | output: github_document 3 | --- 4 | 5 | 6 | 7 | ```{r, include = FALSE} 8 | knitr::opts_chunk$set( 9 | collapse = TRUE, 10 | comment = "#>", 11 | fig.path = "man/figures/README-", 12 | out.width = "100%" 13 | ) 14 | ``` 15 | 16 | # The Data Science StreamRs Calendar! 17 | 18 | 19 | 20 | 21 | In the last few years, we have seen an huge uptick in the number of data science professionals and hobbyists who like to stream their projects and share their knowledge with the community! This calendar is my small attempt to contribute back to this awesome community so you can see what great streams are happening! 22 | 23 | This repository contains the Shiny app providing the streamers calendar, created with the [`{golem}`](https://golemverse.org/) package. In addition, this app would not be possible without the [`{toastui}`](https://github.com/dreamRs/toastui) package by the fine friends at DreamRs. 24 | 25 | ## Installation 26 | 27 | You can install the development version from [GitHub](https://github.com/) with: 28 | 29 | ``` r 30 | # install.packages("remotes") 31 | remotes::install_github("rpodcast/shinycal") 32 | ``` 33 | 34 | ## Code of Conduct 35 | 36 | Please note that the shinycal project is released with a [Contributor Code of Conduct](https://contributor-covenant.org/version/2/0/CODE_OF_CONDUCT.html). By contributing to this project, you agree to abide by its terms. -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | # shinycal 0.8 2 | 3 | * Add a feedback Shiny module so users can easily report issues with using the app directly on GitHub via the `gh` package. Credit to @shannonpileggi for the idea! 4 | 5 | # shinycal 0.7 6 | 7 | * Add custom javascript for detecting the time zone of the user browsing the app, and make that the default time zone when application finishes loading. This solves the bug of time zones not being correctly mapped for those outside of the original America/New_York time zone (#26) 8 | 9 | # shinycal 0.6 10 | 11 | * Account for daylight savings time by adding `ambiguous` param to custom time parser functions 12 | * Add nodejs and puppeteer to the development contrainer for further timezone testing 13 | 14 | # shinycal 0.5.1 15 | 16 | * Organize the time zone choices into groups (thanks, @tanho63 and @PythonCoderUnicorn!) 17 | # shinycal 0.5 18 | 19 | * Add a new time zone selector (powered by `{shinyWidgets}`) 20 | * Revise how the view type changes the calendar view on the backend. The value of this input is now directly fed into the calendar function, instead of changing it via the proxy object. 21 | # shinycal 0.4 22 | 23 | * Move video player for a streamer's latest VOD to the right of the calendar 24 | # shinycal 0.3 25 | 26 | * Added a `NEWS.md` file to track changes to the package. 27 | * Allow user to change the calendar entry background color as well as font color 28 | * Add placeholder margin for new elements 29 | * Restructured reactive components of the calendar display data 30 | -------------------------------------------------------------------------------- /testapp/app.R: -------------------------------------------------------------------------------- 1 | # 2 | # This is a Shiny web application. You can run the application by clicking 3 | # the 'Run App' button above. 4 | # 5 | # Find out more about building applications with Shiny here: 6 | # 7 | # http://shiny.rstudio.com/ 8 | # 9 | 10 | library(shiny) 11 | library(toastui) 12 | library(calendar) 13 | 14 | ics_df <- ic_read("../r_code/wimpys-world-of-streamers-2021-06-08.ics") 15 | 16 | 17 | # Define UI for application that draws a histogram 18 | ui <- fluidPage( 19 | 20 | fluidRow( 21 | column( 22 | width = 12, 23 | calendarOutput("testout", width = "100%", height = "600px") 24 | ) 25 | ) 26 | ) 27 | 28 | # Define server logic required to draw a histogram 29 | server <- function(input, output, session) { 30 | 31 | output$testout <- renderCalendar({ 32 | calendar(cal_demo_data(), useNavigation = TRUE) %>% 33 | cal_props( 34 | list( 35 | id = 1, 36 | name = "PERSO", 37 | color = "white", 38 | bgColor = "firebrick", 39 | borderColor = "firebrick" 40 | ), 41 | list( 42 | id = 2, 43 | name = "WORK", 44 | color = "white", 45 | bgColor = "forestgreen", 46 | borderColor = "forestgreen" 47 | ) 48 | ) 49 | }) 50 | } 51 | 52 | # Run the application 53 | shinyApp(ui = ui, server = server) 54 | -------------------------------------------------------------------------------- /vignettes/github_backend.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "github_backend" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{github_backend} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | ```{r, include = FALSE} 11 | knitr::opts_chunk$set( 12 | collapse = TRUE, 13 | comment = "#>" 14 | ) 15 | ``` 16 | 17 | ```{r setup} 18 | library(usethis) 19 | ``` 20 | 21 | ### Setting up a local copy of repo 22 | 23 | ```{r setup-git} 24 | github_url <- "git@github.com:rpodcast/streamer_data_dev.git" 25 | 26 | #dest_dir <- fs::path("prototyping", "github_data") 27 | dest_dir <- fs::path(fs::path_temp(), "github_data") 28 | 29 | if (fs::dir_exists(dest_dir)) fs::dir_delete(dest_dir) 30 | fs::dir_create(dest_dir) 31 | 32 | create_from_github(github_url, destdir = dest_dir, fork = FALSE, rstudio = FALSE, open = FALSE, protocol = "ssh") 33 | 34 | fs::dir_tree(dest_dir) 35 | ``` 36 | 37 | ### Try sending a PR with a new file 38 | 39 | ```{r pr-from-local} 40 | library(gert) 41 | # make a new yaml file 42 | repo_dir <- fs::path(dest_dir, "streamer_data_dev") 43 | yaml_dir <- fs::path(dest_dir, "streamer_data_dev", "yaml_files") 44 | 45 | fs::dir_ls(yaml_dir) 46 | # copy one for testing 47 | fs::file_copy(fs::path(yaml_dir, "unicoRn_coder.yml"), fs::path(yaml_dir, "live_streamer.yml")) 48 | 49 | withr::with_dir(repo_dir, { 50 | git_add("yaml_files/live_streamer.yml") 51 | git_commit("Add live_streamer.yml") 52 | usethis::pr_init(branch = "live-streamer3") 53 | #usethis::pr_push() 54 | system("git push -u origin live-streamer3") 55 | }) 56 | 57 | 58 | 59 | 60 | ``` 61 | -------------------------------------------------------------------------------- /man/run_app.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/run_app.R 3 | \name{run_app} 4 | \alias{run_app} 5 | \title{Run the Shiny Application} 6 | \usage{ 7 | run_app( 8 | onStart = NULL, 9 | options = list(), 10 | enableBookmarking = NULL, 11 | uiPattern = "/", 12 | ... 13 | ) 14 | } 15 | \arguments{ 16 | \item{onStart}{A function that will be called before the app is actually run. 17 | This is only needed for \code{shinyAppObj}, since in the \code{shinyAppDir} 18 | case, a \code{global.R} file can be used for this purpose.} 19 | 20 | \item{options}{Named options that should be passed to the \code{runApp} call 21 | (these can be any of the following: "port", "launch.browser", "host", "quiet", 22 | "display.mode" and "test.mode"). You can also specify \code{width} and 23 | \code{height} parameters which provide a hint to the embedding environment 24 | about the ideal height/width for the app.} 25 | 26 | \item{enableBookmarking}{Can be one of \code{"url"}, \code{"server"}, or 27 | \code{"disable"}. The default value, \code{NULL}, will respect the setting from 28 | any previous calls to \code{\link[shiny:enableBookmarking]{enableBookmarking()}}. See \code{\link[shiny:enableBookmarking]{enableBookmarking()}} 29 | for more information on bookmarking your app.} 30 | 31 | \item{uiPattern}{A regular expression that will be applied to each \code{GET} 32 | request to determine whether the \code{ui} should be used to handle the 33 | request. Note that the entire request path must match the regular 34 | expression in order for the match to be considered successful.} 35 | 36 | \item{...}{arguments to pass to golem_opts. 37 | See `?golem::get_golem_options` for more details.} 38 | } 39 | \description{ 40 | Run the Shiny Application 41 | } 42 | -------------------------------------------------------------------------------- /dev/python_code/hello.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | import sys 3 | import caldav 4 | import os 5 | from dotenv import load_dotenv 6 | 7 | #https://nextcloud.r-podcastdev.link/index.php/apps/calendar/p/zNyH2RE3pLHctoEf 8 | 9 | # "https://nextcloud03.webo.cloud/remote.php/dav/calendars/me@myweb.de/personal/" 10 | # "https://nextcloud.r-podcastdev.link/remote.php/dav/calendars/rpodcast/wimpys-world-of-streamers/" 11 | #caldav_url = 'https://calendar.example.com/dav' 12 | caldav_url = 'https://nextcloud.r-podcastdev.link/remote.php/dav' 13 | 14 | # load environment variables for user name and password to Nextcloud 15 | load_dotenv() 16 | 17 | client = caldav.DAVClient(url=caldav_url, username=os.getenv("NEXTCLOUD_USER"), password=os.getenv("NEXTCLOUD_PASSWORD")) 18 | my_principal = client.principal() 19 | calendars = my_principal.calendars() 20 | 21 | wimpy_cal = "https://nextcloud.r-podcastdev.link/remote.php/dav/calendars/rpodcast/wimpys-world-of-streamers/" 22 | test_cal = "https://nextcloud.r-podcastdev.link/remote.php/dav/calendars/rpodcast/erics-play-calendar/" 23 | 24 | the_same_calendar = client.calendar(url=wimpy_cal) 25 | 26 | all_events = the_same_calendar.events() 27 | 28 | if all_events: 29 | print("your calendar has %i cevents:" % len(all_events)) 30 | for e in all_events: 31 | print(e) 32 | print(e.data) 33 | else: 34 | print("your calendar has no events") 35 | 36 | 37 | if calendars: 38 | ## Some calendar servers will include all calendars you have 39 | ## access to in this list, and not only the calendars owned by 40 | ## this principal. 41 | print("your principal has %i calendars:" % len(calendars)) 42 | for c in calendars: 43 | print(" Name: %-20s URL: %s" % (c.name, c.url)) 44 | else: 45 | print("your principal has no calendars") 46 | 47 | -------------------------------------------------------------------------------- /dev/03_deploy.R: -------------------------------------------------------------------------------- 1 | # Building a Prod-Ready, Robust Shiny Application. 2 | # 3 | # README: each step of the dev files is optional, and you don't have to 4 | # fill every dev scripts before getting started. 5 | # 01_start.R should be filled at start. 6 | # 02_dev.R should be used to keep track of your development during the project. 7 | # 03_deploy.R should be used once you need to deploy your app. 8 | # 9 | # 10 | ###################################### 11 | #### CURRENT FILE: DEPLOY SCRIPT ##### 12 | ###################################### 13 | 14 | # Test your app 15 | 16 | ## Run checks ---- 17 | ## Check the package before sending to prod 18 | devtools::check() 19 | #rhub::check_for_cran() 20 | 21 | # Deploy 22 | 23 | ## Local, CRAN or Package Manager ---- 24 | ## This will build a tar.gz that can be installed locally, 25 | ## sent to CRAN, or to a package manager 26 | devtools::build() 27 | 28 | ## RStudio ---- 29 | ## If you want to deploy on RStudio related platforms 30 | golem::add_rstudioconnect_file() 31 | golem::add_shinyappsio_file() 32 | golem::add_shinyserver_file() 33 | 34 | ## Docker ---- 35 | ## If you want to deploy via a generic Dockerfile 36 | golem::add_dockerfile() 37 | 38 | ## If you want to deploy to ShinyProxy 39 | golem::add_dockerfile_shinyproxy() 40 | 41 | ## If you want to deploy to Heroku 42 | golem::add_dockerfile_heroku() 43 | 44 | ## deploy app 45 | # if deploying in vs code container, must create /workspaces/rsconnect dir first 46 | # sudo mkdir /workspaces/rsconnect 47 | # sudo chown -R eric:eric /workspaces/rsconnect 48 | 49 | # one-time operation 50 | rsconnect::setAccountInfo( 51 | name = "rpodcast", 52 | token = Sys.getenv("RSCONNECT_TOKEN"), 53 | secret = Sys.getenv("RSCONNECT_SECRET") 54 | ) 55 | 56 | library(gert) 57 | current_branch <- git_branch() 58 | app_name <- golem::get_golem_name() 59 | 60 | if (current_branch != "master") { 61 | app_name <- paste0(app_name, "_dev") 62 | } 63 | 64 | rsconnect::deployApp( 65 | appName = app_name, 66 | appFileManifest = "dev/app_manifest.txt", 67 | launch.browser = FALSE, 68 | forceUpdate = TRUE 69 | ) 70 | -------------------------------------------------------------------------------- /dev/hello.R: -------------------------------------------------------------------------------- 1 | library(purrr) 2 | 3 | # these should not be needed now that I set the python path env var via .Rprofile 4 | #reticulate::use_python("/usr/bin/python3") 5 | #reticulate::py_config() 6 | #reticulate::py_available() 7 | 8 | library(reticulate) 9 | 10 | caldav_url = 'https://nextcloud.r-podcastdev.link/remote.php/dav' 11 | 12 | caldav <- import("caldav") 13 | 14 | client <- caldav$DAVClient(url = caldav_url, username = Sys.getenv("NEXTCLOUD_USER"), password = Sys.getenv("NEXTCLOUD_PASSWORD")) 15 | 16 | my_principal <- client$principal() 17 | calendars <- my_principal$calendars() 18 | 19 | purrr::walk(calendars, ~{ 20 | message(glue::glue("Name {.x$name} URL {.x$url}")) 21 | }) 22 | 23 | per_cal_url <- "https://nextcloud.r-podcastdev.link/remote.php/dav/calendars/rpodcast/personal/" 24 | wimpy_cal_url <- "https://nextcloud.r-podcastdev.link/remote.php/dav/calendars/rpodcast/wimpys-world-of-streamers/" 25 | test_cal_url <- "https://nextcloud.r-podcastdev.link/remote.php/dav/calendars/rpodcast/erics-play-calendar/" 26 | 27 | wimpy_cal <- client$calendar(url = wimpy_cal_url) 28 | wimpy_events <- wimpy_cal$events() 29 | 30 | one_event <- wimpy_events[[1]] 31 | 32 | 33 | data_obj <- one_event$data 34 | 35 | one_event$get_property(prop = "DTSTART") 36 | 37 | 38 | library(calendar) 39 | ical_example = readLines(system.file("extdata", "example.ics", package = "calendar")) 40 | 41 | x <- ic_attributes_vec(x = ical_example) 42 | 43 | ics_df <- ic_read(system.file("extdata", "example.ics", package = "calendar")) 44 | ics_df <- ic_read("r_code/wimpys-world-of-streamers-2021-06-08.ics") 45 | 46 | library(caldav) 47 | 48 | caldav_url = "https://nextcloud.r-podcastdev.link/remote.php/dav/calendars/rpodcast/wimpys-world-of-streamers/" 49 | cal_data <- 50 | caldav_get_all_simple_auth( 51 | url = caldav_url, 52 | user = Sys.getenv("NEXTCLOUD_USER"), 53 | password = Sys.getenv("NEXTCLOUD_PASSWORD") 54 | ) 55 | 56 | x <- cal_data$calendar 57 | 58 | cat(x, file = "r_code/tmp.ics") 59 | 60 | 61 | library(purrr) 62 | tidy_cal <- purrr::map(x, ~stringr::str_split(.x, "\\n")) 63 | 64 | ic_read("r_code/tmp.ics") 65 | 66 | 67 | tidy_cal[[1]] 68 | -------------------------------------------------------------------------------- /dev/r_code/hello.R: -------------------------------------------------------------------------------- 1 | library(purrr) 2 | 3 | # these should not be needed now that I set the python path env var via .Rprofile 4 | #reticulate::use_python("/usr/bin/python3") 5 | #reticulate::py_config() 6 | #reticulate::py_available() 7 | 8 | library(reticulate) 9 | 10 | caldav_url = 'https://nextcloud.r-podcastdev.link/remote.php/dav' 11 | 12 | caldav <- import("caldav") 13 | 14 | client <- caldav$DAVClient(url = caldav_url, username = Sys.getenv("NEXTCLOUD_USER"), password = Sys.getenv("NEXTCLOUD_PASSWORD")) 15 | 16 | my_principal <- client$principal() 17 | calendars <- my_principal$calendars() 18 | 19 | purrr::walk(calendars, ~{ 20 | message(glue::glue("Name {.x$name} URL {.x$url}")) 21 | }) 22 | 23 | per_cal_url <- "https://nextcloud.r-podcastdev.link/remote.php/dav/calendars/rpodcast/personal/" 24 | wimpy_cal_url <- "https://nextcloud.r-podcastdev.link/remote.php/dav/calendars/rpodcast/wimpys-world-of-streamers/" 25 | test_cal_url <- "https://nextcloud.r-podcastdev.link/remote.php/dav/calendars/rpodcast/erics-play-calendar/" 26 | 27 | wimpy_cal <- client$calendar(url = wimpy_cal_url) 28 | wimpy_events <- wimpy_cal$events() 29 | 30 | one_event <- wimpy_events[[1]] 31 | 32 | 33 | data_obj <- one_event$data 34 | 35 | one_event$get_property(prop = "DTSTART") 36 | 37 | 38 | library(calendar) 39 | ical_example = readLines(system.file("extdata", "example.ics", package = "calendar")) 40 | 41 | x <- ic_attributes_vec(x = ical_example) 42 | 43 | ics_df <- ic_read(system.file("extdata", "example.ics", package = "calendar")) 44 | ics_df <- ic_read("r_code/wimpys-world-of-streamers-2021-06-08.ics") 45 | 46 | library(caldav) 47 | 48 | caldav_url = "https://nextcloud.r-podcastdev.link/remote.php/dav/calendars/rpodcast/wimpys-world-of-streamers/" 49 | cal_data <- 50 | caldav_get_all_simple_auth( 51 | url = caldav_url, 52 | user = Sys.getenv("NEXTCLOUD_USER"), 53 | password = Sys.getenv("NEXTCLOUD_PASSWORD") 54 | ) 55 | 56 | x <- cal_data$calendar 57 | 58 | cat(x, file = "r_code/tmp.ics") 59 | 60 | 61 | library(purrr) 62 | tidy_cal <- purrr::map(x, ~stringr::str_split(.x, "\\n")) 63 | 64 | ic_read("r_code/tmp.ics") 65 | 66 | 67 | tidy_cal[[1]] 68 | -------------------------------------------------------------------------------- /R/app_ui.R: -------------------------------------------------------------------------------- 1 | #' The application User-Interface 2 | #' 3 | #' @param request Internal parameter for `{shiny}`. 4 | #' DO NOT REMOVE. 5 | #' @import shiny 6 | #' @noRd 7 | app_ui <- function(request) { 8 | tagList( 9 | # Leave this function for adding external resources 10 | golem_add_external_resources(), 11 | # Your application UI logic 12 | navbarPage( 13 | title = "shinycal", 14 | tabPanel( 15 | title = "Calendar", 16 | value = "calendar", 17 | HTML(' '), 18 | HTML(' '), 19 | HTML(' '), 20 | fluidRow( 21 | col_12( 22 | h1("The Data Science StreamRs Calendar!"), 23 | shiny::includeMarkdown(app_sys("app", "www", "intro.md")), 24 | ) 25 | ), 26 | fluidRow( 27 | col_12( 28 | mod_cal_viewer_ui("cal_viewer_ui_1") 29 | ) 30 | ) 31 | ), 32 | tabPanel( 33 | title = "Entry", 34 | value = "entry", 35 | h1("Under Construction!") 36 | #mod_cal_entry_ui("cal_entry_ui_1") 37 | ), 38 | tabPanel( 39 | title = "Feedback", 40 | value = "feedback", 41 | mod_feedback_ui("feedback_ui_1") 42 | ) 43 | ) 44 | ) 45 | } 46 | 47 | #' Add external Resources to the Application 48 | #' 49 | #' This function is internally used to add external 50 | #' resources inside the Shiny application. 51 | #' 52 | #' @import shiny 53 | #' @importFrom golem add_resource_path activate_js favicon bundle_resources 54 | #' @noRd 55 | golem_add_external_resources <- function(){ 56 | 57 | add_resource_path( 58 | 'www', app_sys('app/www') 59 | ) 60 | 61 | tags$head( 62 | favicon(), 63 | bundle_resources( 64 | path = app_sys('app/www'), 65 | app_title = 'shinycal' 66 | ), 67 | tags$script(src="https://player.twitch.tv/js/embed/v1.js") 68 | # Add here other external resources 69 | # for example, you can add shinyalert::useShinyalert() 70 | ) 71 | } 72 | 73 | -------------------------------------------------------------------------------- /.Rprofile: -------------------------------------------------------------------------------- 1 | 2 | # setup if using with vscode and R plugin 3 | if (Sys.getenv("TERM_PROGRAM") == "vscode") { 4 | source(file.path(Sys.getenv(if (.Platform$OS.type == "windows") "USERPROFILE" else "HOME"), ".vscode-R", "init.R")) 5 | } 6 | source("renv/activate.R") 7 | 8 | if (Sys.getenv("TERM_PROGRAM") == "vscode") { 9 | # obtain list of packages in renv library currently 10 | project <- renv:::renv_project_resolve(NULL) 11 | lib_packages <- names(unclass(renv:::renv_diagnostics_packages_library(project))$Packages) 12 | 13 | # detect whether key packages are already installed 14 | # was: !require("languageserver") 15 | if (!"languageserver" %in% lib_packages) { 16 | message("installing languageserver package") 17 | renv::install("languageserver") 18 | } 19 | 20 | if (!"httpgd" %in% lib_packages) { 21 | message("installing httpgd package") 22 | renv::install("httpgd") 23 | } 24 | 25 | if (!"vscDebugger" %in% lib_packages) { 26 | message("installation vscDebugger package") 27 | renv::install("ManuelHentschel/vscDebugger@v0.4.7") 28 | } 29 | 30 | # use the rstudio addins feature 31 | if (!"rstudioapi" %in% lib_packages) { 32 | message("installing rstudioapi package") 33 | renv::install("rstudioapi") 34 | } 35 | options(vsc.rstudioapi = TRUE) 36 | 37 | # use the new httpgd plotting device 38 | options(vsc.plot = FALSE) 39 | options(device = function(...) { 40 | httpgd:::hgd() 41 | .vsc.browser(httpgd::hgd_url(), viewer = "Beside") 42 | }) 43 | } 44 | 45 | # uncomment this section if using a pulseaudio server from a linux host 46 | play_sound <- function(sound_dir = "/soundboard_files", custom_sink = "SoundBoard", obs_animate = TRUE, wait = FALSE) { 47 | audio_file <- sample(list.files(sound_dir, full.names = TRUE), size = 1) 48 | play_args <- c(audio_file) 49 | 50 | if (!is.null(custom_sink)) { 51 | play_args <- c(play_args, "-d", custom_sink) 52 | } 53 | 54 | system2("paplay", args = play_args, wait = wait) 55 | 56 | if (obs_animate) { 57 | system2("curl", paste0("http://192.168.1.178:1030/image?filename=/", audio_file), stdout = FALSE, stderr = FALSE) 58 | } 59 | 60 | invisible(TRUE) 61 | } 62 | 63 | options(error = play_sound) -------------------------------------------------------------------------------- /dev/01_start.R: -------------------------------------------------------------------------------- 1 | # Building a Prod-Ready, Robust Shiny Application. 2 | # 3 | # README: each step of the dev files is optional, and you don't have to 4 | # fill every dev scripts before getting started. 5 | # 01_start.R should be filled at start. 6 | # 02_dev.R should be used to keep track of your development during the project. 7 | # 03_deploy.R should be used once you need to deploy your app. 8 | # 9 | # 10 | ######################################## 11 | #### CURRENT FILE: ON START SCRIPT ##### 12 | ######################################## 13 | 14 | ## Fill the DESCRIPTION ---- 15 | ## Add meta data about your application 16 | ## 17 | ## /!\ Note: if you want to change the name of your app during development, 18 | ## either re-run this function, call golem::set_golem_name(), or don't forget 19 | ## to change the name in the app_sys() function in app_config.R /!\ 20 | ## 21 | golem::fill_desc( 22 | pkg_name = "shinycal", # The Name of the package containing the App 23 | pkg_title = "PKG_TITLE", # The Title of the package containing the App 24 | pkg_description = "PKG_DESC.", # The Description of the package containing the App 25 | author_first_name = "AUTHOR_FIRST", # Your First Name 26 | author_last_name = "AUTHOR_LAST", # Your Last Name 27 | author_email = "AUTHOR@MAIL.COM", # Your Email 28 | repo_url = "https://github.com/rpodcast/shinycal" # The URL of the GitHub Repo (optional) 29 | ) 30 | 31 | ## Set {golem} options ---- 32 | golem::set_golem_options() 33 | 34 | ## Create Common Files ---- 35 | ## See ?usethis for more information 36 | usethis::use_mit_license( "Eric Nantz" ) # You can set another license here 37 | usethis::use_readme_rmd( open = FALSE ) 38 | usethis::use_code_of_conduct() 39 | usethis::use_lifecycle_badge( "Experimental" ) 40 | usethis::use_news_md( open = FALSE ) 41 | 42 | ## Use git ---- 43 | usethis::use_git() 44 | 45 | ## Init Testing Infrastructure ---- 46 | ## Create a template for tests 47 | golem::use_recommended_tests() 48 | 49 | ## Use Recommended Packages ---- 50 | golem::use_recommended_deps() 51 | 52 | ## Favicon ---- 53 | # If you want to change the favicon (default is golem's one) 54 | golem::use_favicon() # path = "path/to/ico". Can be an online file. 55 | golem::remove_favicon() 56 | 57 | ## Add helper functions ---- 58 | golem::use_utils_ui() 59 | golem::use_utils_server() 60 | 61 | # You're now set! ---- 62 | 63 | # go to dev/02_dev.R 64 | rstudioapi::navigateToFile( "dev/02_dev.R" ) 65 | 66 | -------------------------------------------------------------------------------- /.devcontainer/rstudio_docker/Dockerfile: -------------------------------------------------------------------------------- 1 | ################################################################################################################################################################## 2 | # Adapted from following: 3 | # - Rocker RStudio container using new versioned paradigm: https://github.com/rocker-org/rocker-versioned2/blob/master/dockerfiles/Dockerfile_rstudio_4.0.2 4 | # - license: GPLV2 5 | ################################################################################################################################################################## 6 | 7 | FROM rocker/r-ver:4.1.0 8 | 9 | ENV S6_VERSION=v1.21.7.0 10 | ENV RSTUDIO_VERSION=preview 11 | ENV PATH=/usr/lib/rstudio-server/bin:$PATH 12 | 13 | # install key dependencies of rstudio preview edition 14 | # install radian via python and pip3 15 | RUN apt-get update \ 16 | && export DEBIAN_FRONTEND=noninteractive \ 17 | && apt-get -y install --no-install-recommends libpq5 openssh-client openssh-server 18 | 19 | RUN /rocker_scripts/install_rstudio.sh 20 | RUN /rocker_scripts/install_pandoc.sh 21 | 22 | # install key dependencies of certain packages that could be installed later 23 | RUN apt-get update \ 24 | && export DEBIAN_FRONTEND=noninteractive \ 25 | && apt-get -y install --no-install-recommends software-properties-common libssl-dev libxml2-dev libcurl4-openssl-dev python3-pip pandoc \ 26 | && apt-get -y install --no-install-recommends libfreetype6-dev libfontconfig1-dev tk libgit2-dev \ 27 | && apt-get autoremove -y && apt-get clean -y && rm -rf /var/lib/apt/lists/* 28 | 29 | # install R packages needed for VSCode interaction and package management 30 | RUN install2.r languageserver renv remotes reticulate golem crul curl httr xml2 cachem shinyWidgets dplyr purrr ggplot2 tidyr fs glue calendar devtools rmarkdown clock 31 | RUN installGithub.r dreamRs/toastui ColinFay/brochure ColinFay/glouton petermeissner/caldav koderkow/twitchr 32 | 33 | # install radian via python and pip3 34 | RUN apt-get update \ 35 | && export DEBIAN_FRONTEND=noninteractive \ 36 | && apt-get -y install --no-install-recommends python3-setuptools python3-pip libpython3-dev 37 | 38 | RUN pip3 install radian caldav icalendar numpy 39 | 40 | RUN echo "RENV_PATHS_CACHE=/renv/cache" >> /usr/local/lib/R/etc/Renviron 41 | 42 | # [Optional] Uncomment this section to add addtional system dependencies needed for project 43 | # RUN apt-get update \ 44 | # && export DEBIAN_FRONTEND=noninteractive \ 45 | # && apt-get -y install --no-install-recommends ---packages list---- 46 | 47 | EXPOSE 8787 48 | 49 | CMD ["/init"] 50 | -------------------------------------------------------------------------------- /dev/02_dev.R: -------------------------------------------------------------------------------- 1 | # Building a Prod-Ready, Robust Shiny Application. 2 | # 3 | # README: each step of the dev files is optional, and you don't have to 4 | # fill every dev scripts before getting started. 5 | # 01_start.R should be filled at start. 6 | # 02_dev.R should be used to keep track of your development during the project. 7 | # 03_deploy.R should be used once you need to deploy your app. 8 | # 9 | # 10 | ################################### 11 | #### CURRENT FILE: DEV SCRIPT ##### 12 | ################################### 13 | 14 | # Engineering 15 | 16 | ## Dependencies ---- 17 | ## Add one line by package you want to add as dependency 18 | usethis::use_package("calendar") 19 | usethis::use_dev_package("petermeissner/caldav") 20 | usethis::use_dev_package("dreamRs/toastui") 21 | usethis::use_package("purrr") 22 | usethis::use_package("dplyr") 23 | 24 | 25 | ## Add modules ---- 26 | ## Create a module infrastructure in R/ 27 | golem::add_module( name = "cal_viewer" ) # Name of the module 28 | golem::add_module(name = "cal_entry") 29 | 30 | ## Add helper functions ---- 31 | ## Creates fct_* and utils_* 32 | golem::add_fct( "helpers", module = "cal_viewer") 33 | golem::add_utils( "helpers" ) 34 | 35 | ## External resources 36 | ## Creates .js and .css files at inst/app/www 37 | golem::add_js_file( "script" ) 38 | golem::add_js_handler( "handlers" ) 39 | golem::add_css_file( "custom" ) 40 | 41 | ## Add internal datasets ---- 42 | ## If you have data in your package 43 | usethis::use_data_raw( name = "streamer_data", open = FALSE ) 44 | 45 | ## Tests ---- 46 | ## Add one line by test you want to create 47 | usethis::use_test( "app" ) 48 | 49 | # Documentation 50 | 51 | ## Vignette ---- 52 | usethis::use_vignette("tidying_calendar") 53 | usethis::use_vignette("streamer_info") 54 | 55 | devtools::build_vignettes() 56 | 57 | ## Code Coverage---- 58 | ## Set the code coverage service ("codecov" or "coveralls") 59 | usethis::use_coverage() 60 | 61 | # Create a summary readme for the testthat subdirectory 62 | covrpage::covrpage() 63 | 64 | ## CI ---- 65 | ## Use this part of the script if you need to set up a CI 66 | ## service for your application 67 | ## 68 | ## (You'll need GitHub there) 69 | usethis::use_github() 70 | 71 | # GitHub Actions 72 | usethis::use_github_action() 73 | # Chose one of the three 74 | # See https://usethis.r-lib.org/reference/use_github_action.html 75 | usethis::use_github_action_check_release() 76 | usethis::use_github_action_check_standard() 77 | usethis::use_github_action_check_full() 78 | # Add action for PR 79 | usethis::use_github_action_pr_commands() 80 | 81 | # Travis CI 82 | usethis::use_travis() 83 | usethis::use_travis_badge() 84 | 85 | # AppVeyor 86 | usethis::use_appveyor() 87 | usethis::use_appveyor_badge() 88 | 89 | # Circle CI 90 | usethis::use_circleci() 91 | usethis::use_circleci_badge() 92 | 93 | # Jenkins 94 | usethis::use_jenkins() 95 | 96 | # GitLab CI 97 | usethis::use_gitlab_ci() 98 | 99 | # You're now set! ---- 100 | # go to dev/03_deploy.R 101 | rstudioapi::navigateToFile("dev/03_deploy.R") 102 | 103 | -------------------------------------------------------------------------------- /R/mod_cal_entry.R: -------------------------------------------------------------------------------- 1 | #' cal_entry UI Function 2 | #' 3 | #' @description A shiny Module. 4 | #' 5 | #' @param id,input,output,session Internal parameters for {shiny}. 6 | #' 7 | #' @noRd 8 | #' 9 | #' @importFrom shiny NS tagList 10 | #' @import shinyvalidate 11 | mod_cal_entry_ui <- function(id){ 12 | ns <- NS(id) 13 | tagList( 14 | fluidRow( 15 | col_6( 16 | textInput( 17 | ns("user_id"), 18 | with_red_star("Twitch user ID"), 19 | value = "", 20 | placeholder = "Please enter an ID" 21 | ), 22 | textInput( 23 | ns("user_name"), 24 | with_red_star("Enter name"), 25 | value = "" 26 | ), 27 | textInput( 28 | ns("twitter_id"), 29 | "Twitter ID (Optional)", 30 | value = "" 31 | ), 32 | shinyWidgets::prettyRadioButtons( 33 | ns("platform"), 34 | label = "Choose Streaming Platform", 35 | choices = c("twitch", "youtube"), 36 | selected = "twitch" 37 | ), 38 | shinyWidgets::pickerInput( 39 | ns("categories"), 40 | label = with_red_star("Select categories"), 41 | choices = c("R", "datascience", "shiny", "sports", "gaming", "linux", "julia", "python"), 42 | selected = "R", 43 | options = list( 44 | `actions-box` = TRUE), 45 | multiple = TRUE 46 | ), 47 | shinyWidgets::actionBttn( 48 | ns("submit"), 49 | label = "Submit!", 50 | icon = "rocket", 51 | style = "jelly", 52 | color = "success", 53 | size = "lg" 54 | ), 55 | verbatimTextOutput( 56 | ns("debugging") 57 | ) 58 | ) 59 | ) 60 | ) 61 | } 62 | 63 | #' cal_entry Server Functions 64 | #' 65 | #' @noRd 66 | mod_cal_entry_server <- function(id){ 67 | moduleServer( id, function(input, output, session){ 68 | ns <- session$ns 69 | 70 | # reactive vals 71 | entry_yaml <- reactiveVal(NULL) 72 | 73 | # establish form validation processing and rules 74 | iv <- InputValidator$new() 75 | 76 | iv$add_rule("user_id", sv_required()) 77 | iv$add_rule("user_name", sv_required()) 78 | iv$add_rule("categories", sv_required()) 79 | 80 | # process form submission 81 | observeEvent(input$submit, { 82 | # ensure form is valid before processing 83 | iv$enable() 84 | 85 | if (!iv$is_valid()) { 86 | shinyWidgets::show_alert( 87 | title = "Oops", 88 | text = "Please complete the form" 89 | ) 90 | return(NULL) 91 | } 92 | 93 | req(iv$is_valid()) 94 | 95 | # assemble form inputs and create yaml structure 96 | entry_list <- list( 97 | user_id = input$user_id, 98 | user_name = input$user_name, 99 | twitter_id = input$twitter_id, 100 | platform = input$platform, 101 | categories = input$categories 102 | ) 103 | 104 | en_yaml <- yaml::as.yaml(entry_list) 105 | entry_yaml(en_yaml) 106 | }) 107 | 108 | output$debugging <- renderPrint({ 109 | req(entry_yaml()) 110 | cat(entry_yaml()) 111 | }) 112 | 113 | }) 114 | } 115 | 116 | ## To be copied in the UI 117 | # mod_cal_entry_ui("cal_entry_ui_1") 118 | 119 | ## To be copied in the server 120 | # mod_cal_entry_server("cal_entry_ui_1") 121 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/vscode-remote/devcontainer.json or this file's README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.134.1/containers/r 3 | { 4 | "name": "R", 5 | "build": { 6 | "dockerfile": "Dockerfile", 7 | "args": { 8 | "USERNAME": "${localEnv:USER}" 9 | } 10 | }, 11 | "runArgs": [ "--cap-add=SYS_PTRACE", "--security-opt", "seccomp=unconfined", "--env-file", ".devcontainer/.env" ], 12 | 13 | // Add more local mounts to container 14 | // The first mounts are for a local pulseaudio server on linux 15 | // You can leave these commented out or delete if you are not linking the contianer to a linux sound system 16 | "mounts": [ 17 | "source=/etc/alsa,target=/etc/alsa,type=bind,consistency=cached", 18 | "source=/usr/share/alsa,target=/usr/share/alsa,type=bind,consistency=cached", 19 | "source=/run/user/1000/pulse/native,target=/run/user/1000/pulse/native,type=bind,consistency=cached", 20 | "source=/media/media_drive1/obs_files/audio_assets/soundboard_files,target=/soundboard_files,type=bind,consistency=cached", 21 | "source=/opt/local/renv/cache,target=/renv/cache,type=bind,consistency=cached" 22 | ], 23 | // Configure environment variables for audio sound server if needed 24 | // You can leave these commented out or delete if you are not linking the contianer to a linux sound system 25 | "containerEnv": { 26 | "PULSE_SERVER": "unix:/run/user/1000/pulse/native" 27 | }, 28 | 29 | // Set *default* container specific settings.json values on container create. 30 | "settings": { 31 | "terminal.integrated.profiles.linux": { 32 | "fish": { 33 | "path": "/usr/bin/fish" 34 | } 35 | }, 36 | "terminal.integrated.defaultProfile.linux": "fish", 37 | "r.alwaysUseActiveTerminal": true, 38 | "r.bracketedPaste": true, 39 | "r.sessionWatcher": true, 40 | "r.rterm.linux": "/usr/local/bin/radian", 41 | "r.rterm.option": [ 42 | "" 43 | ], 44 | "r.autoDetect": "false", 45 | "r.terminalPath": "/usr/local/bin/radian", 46 | "r.interpreterPath": "/usr/local/bin/R", 47 | "r.debugger.timeouts.startup": 8000, 48 | "editor.wordWrap": "on", 49 | "editor.tabSize": 2, 50 | "path-autocomplete.pathMappings": { 51 | "/": "/", 52 | "./": "${folder}" 53 | }, 54 | "editor.bracketPairColorization.enabled": true, 55 | "editor.guides.bracketPairs": "active" 56 | }, 57 | 58 | // Add the IDs of extensions you want installed when the container is created. 59 | "extensions": [ 60 | "ikuyadeu.r", 61 | //"/renv/cache/r-latest.vslx", 62 | "ionutvmi.path-autocomplete", 63 | "usernamehw.errorlens", 64 | "mhutchie.git-graph", 65 | "wayou.vscode-todo-highlight", 66 | "tomoki1207.pdf", 67 | "DavidAnson.vscode-markdownlint", 68 | "Rubymaniac.vscode-paste-and-indent", 69 | "GrapeCity.gc-excelviewer", 70 | "IBM.output-colorizer", 71 | "Mohamed-El-Fodil-Ihaddaden.shinysnip", 72 | "hediet.vscode-drawio", 73 | "MS-vsliveshare.vsliveshare-pack", 74 | "ms-python.python", 75 | "RDebugger.r-debugger", 76 | //"GitHub.copilot", 77 | "eamodio.gitlens", 78 | "GitHub.vscode-pull-request-github", 79 | "hoovercj.vscode-power-mode" 80 | ] 81 | 82 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 83 | // "forwardPorts": [], 84 | 85 | // Use 'postCreateCommand' to run commands after the container is created. 86 | // "postCreateCommand": "R --version", 87 | 88 | // Uncomment to connect as a non-root user. See https://aka.ms/vscode-remote/containers/non-root. 89 | // "remoteUser": "docker" 90 | } 91 | -------------------------------------------------------------------------------- /R/mod_feedback.R: -------------------------------------------------------------------------------- 1 | #' feedback UI Function 2 | #' 3 | #' @description A shiny Module. 4 | #' 5 | #' @param id,input,output,session Internal parameters for {shiny}. 6 | #' 7 | #' @noRd 8 | #' 9 | #' @importFrom shiny NS tagList 10 | mod_feedback_ui <- function(id){ 11 | ns <- NS(id) 12 | tagList( 13 | fluidRow( 14 | col_12( 15 | h2("Submit Feedback!"), 16 | p("Use the form below to provide feedback to the author of shinycal directly on the issue tracker.") 17 | ) 18 | ), 19 | fluidRow( 20 | col_6( 21 | textInput( 22 | ns("issue_title"), 23 | with_red_star("Title"), 24 | value = "", 25 | placeholder = "enter short title", 26 | width = "100%" 27 | ) 28 | ), 29 | col_6( 30 | shinyWidgets::pickerInput( 31 | ns("issue_labels"), 32 | label = with_red_star("Choose one or more labels"), 33 | choices = c("silly"), 34 | multiple = TRUE 35 | ) 36 | ) 37 | ), 38 | fluidRow( 39 | col_12( 40 | textAreaInput( 41 | ns("issue_description"), 42 | label = with_red_star("Enter issue description"), 43 | value = "", 44 | width = "300%", 45 | cols = 80, 46 | rows = 10, 47 | placeholder = "markdown format supported", 48 | resize = "vertical" 49 | ) 50 | ) 51 | ), 52 | fluidRow( 53 | col_2( 54 | shinyWidgets::actionBttn( 55 | ns("submit_issue"), 56 | "Submit!", 57 | icon = icon("save"), 58 | style = "jelly", 59 | color = "success", 60 | size = "sm" 61 | ) 62 | ) 63 | ) 64 | ) 65 | } 66 | 67 | #' feedback Server Functions 68 | #' 69 | #' @noRd 70 | mod_feedback_server <- function(id){ 71 | moduleServer( id, function(input, output, session){ 72 | ns <- session$ns 73 | 74 | # update issue label choices 75 | shinyWidgets::updatePickerInput( 76 | session, 77 | "issue_labels", 78 | choices = get_issue_labels(), 79 | selected = NULL 80 | ) 81 | 82 | # process submit click 83 | observeEvent(input$submit_issue, { 84 | # perform mandatory input checks 85 | if (!shiny::isTruthy(input$issue_title)) { 86 | shinyWidgets::sendSweetAlert( 87 | session = session, 88 | title = "Oops!", 89 | text = "Please enter a title for your issue", 90 | type = "error" 91 | ) 92 | 93 | return(NULL) 94 | } 95 | 96 | if (!shiny::isTruthy(input$issue_labels)) { 97 | shinyWidgets::sendSweetAlert( 98 | session = session, 99 | title = "Oops!", 100 | text = "Please choose at least one label", 101 | type = "error" 102 | ) 103 | 104 | return(NULL) 105 | } 106 | 107 | if (!shiny::isTruthy(input$issue_description)) { 108 | shinyWidgets::sendSweetAlert( 109 | session = session, 110 | title = "Oops!", 111 | text = "Please enter an issue description", 112 | type = "error" 113 | ) 114 | 115 | return(NULL) 116 | } 117 | 118 | # process submit 119 | res <- submit_issue( 120 | input$issue_title, 121 | input$issue_labels, 122 | input$issue_description 123 | ) 124 | 125 | # show confirmation 126 | shinyWidgets::sendSweetAlert( 127 | session = session, 128 | title = "Issue submitted!", 129 | text = "Thank you for your feedback on shinycal! Your issue has been submitted to the shinycal GitHub issue tracker.", 130 | type = "success" 131 | ) 132 | 133 | # reset the form inputs 134 | updateTextInput( 135 | session, 136 | "issue_title", 137 | value = "" 138 | ) 139 | 140 | updateTextAreaInput( 141 | session, 142 | "issue_description", 143 | value = "" 144 | ) 145 | 146 | shinyWidgets::updatePickerInput( 147 | session, 148 | "issue_labels", 149 | selected = NULL 150 | ) 151 | }) 152 | }) 153 | } 154 | 155 | ## To be copied in the UI 156 | # mod_feedback_ui("feedback_ui_1") 157 | 158 | ## To be copied in the server 159 | # mod_feedback_server("feedback_ui_1") 160 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the overall 26 | community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards 42 | of acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies 54 | when an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail 56 | address, posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at [INSERT CONTACT 63 | METHOD]. All complaints will be reviewed and investigated promptly and fairly. 64 | 65 | All community leaders are obligated to respect the privacy and security of the 66 | reporter of any incident. 67 | 68 | ## Enforcement Guidelines 69 | 70 | Community leaders will follow these Community Impact Guidelines in determining 71 | the consequences for any action they deem in violation of this Code of Conduct: 72 | 73 | ### 1. Correction 74 | 75 | **Community Impact**: Use of inappropriate language or other behavior deemed 76 | unprofessional or unwelcome in the community. 77 | 78 | **Consequence**: A private, written warning from community leaders, providing 79 | clarity around the nature of the violation and an explanation of why the 80 | behavior was inappropriate. A public apology may be requested. 81 | 82 | ### 2. Warning 83 | 84 | **Community Impact**: A violation through a single incident or series of 85 | actions. 86 | 87 | **Consequence**: A warning with consequences for continued behavior. No 88 | interaction with the people involved, including unsolicited interaction with 89 | those enforcing the Code of Conduct, for a specified period of time. This 90 | includes avoiding interactions in community spaces as well as external channels 91 | like social media. Violating these terms may lead to a temporary or permanent 92 | ban. 93 | 94 | ### 3. Temporary Ban 95 | 96 | **Community Impact**: A serious violation of community standards, including 97 | sustained inappropriate behavior. 98 | 99 | **Consequence**: A temporary ban from any sort of interaction or public 100 | communication with the community for a specified period of time. No public or 101 | private interaction with the people involved, including unsolicited interaction 102 | with those enforcing the Code of Conduct, is allowed during this period. 103 | Violating these terms may lead to a permanent ban. 104 | 105 | ### 4. Permanent Ban 106 | 107 | **Community Impact**: Demonstrating a pattern of violation of community 108 | standards, including sustained inappropriate behavior, harassment of an 109 | individual, or aggression toward or disparagement of classes of individuals. 110 | 111 | **Consequence**: A permanent ban from any sort of public interaction within the 112 | community. 113 | 114 | ## Attribution 115 | 116 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 117 | version 2.0, 118 | available at https://www.contributor-covenant.org/version/2/0/ 119 | code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at https:// 128 | www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | ################################################################################################################### 2 | # Adapted from https://github.com/microsoft/vscode-dev-containers/blob/master/containers/r/.devcontainer/Dockerfile 3 | # licence: MIT 4 | ################################################################################################################### 5 | 6 | FROM rocker/r-ver:4.1.0 7 | 8 | # Options for setup script 9 | ARG INSTALL_ZSH="false" 10 | ARG INSTALL_FISH="true" 11 | ARG UPGRADE_PACKAGES="true" 12 | ARG USERNAME=eric 13 | ARG USER_UID=1000 14 | ARG USER_GID=$USER_UID 15 | 16 | # Remove the docker user since I don't need it for VSCode containers 17 | #RUN groupmod docker --gid 1005 \ 18 | # && usermod --uid 1005 docker 19 | 20 | # Install needed packages and setup non-root user. Use a separate RUN statement to add your own dependencies. 21 | COPY library-scripts/*.sh /tmp/library-scripts/ 22 | RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 23 | && /bin/bash /tmp/library-scripts/common-debian.sh "${INSTALL_ZSH}" "${INSTALL_FISH}" "${USERNAME}" "${USER_UID}" "${USER_GID}" "${UPGRADE_PACKAGES}" \ 24 | && apt-get -y install libzip-dev \ 25 | && apt-get autoremove -y && apt-get clean -y && rm -rf /var/lib/apt/lists/* 26 | 27 | # [Optional] Uncomment this section to install additional OS packages. 28 | # key dependencies for certain R packages 29 | RUN apt-get update \ 30 | && export DEBIAN_FRONTEND=noninteractive \ 31 | && apt-get -y install --no-install-recommends software-properties-common libssl-dev libxml2-dev libcurl4-openssl-dev python3-pip pandoc \ 32 | && apt-get -y install --no-install-recommends libfreetype6-dev libfontconfig1-dev tk libgit2-dev \ 33 | && apt-get autoremove -y && apt-get clean -y && rm -rf /var/lib/apt/lists/* 34 | 35 | # install R packages needed for VSCode interaction and package management 36 | RUN install2.r languageserver renv remotes reticulate golem crul curl httr xml2 cachem shinyWidgets dplyr purrr ggplot2 tidyr fs glue calendar devtools rmarkdown clock 37 | RUN installGithub.r dreamRs/toastui ColinFay/brochure ColinFay/glouton petermeissner/caldav koderkow/twitchr 38 | 39 | # install radian via python and pip3 40 | RUN apt-get update \ 41 | && export DEBIAN_FRONTEND=noninteractive \ 42 | && apt-get -y install --no-install-recommends python3-setuptools 43 | 44 | RUN pip3 install radian caldav icalendar numpy python-dotenv 45 | 46 | # install dot net core runtime for the R Tools plugin 47 | RUN wget https://packages.microsoft.com/config/ubuntu/20.04/packages-microsoft-prod.deb -O /tmp/packages-microsoft-prod.deb \ 48 | && dpkg -i /tmp/packages-microsoft-prod.deb 49 | 50 | RUN apt-get update \ 51 | && apt-get -y install --no-install-recommends apt-transport-https \ 52 | && apt-get -y install --no-install-recommends dotnet-sdk-3.1 53 | 54 | # ensure that the renv package cache env var is accessible in default R installation 55 | RUN echo "RENV_PATHS_CACHE=/renv/cache" >> /usr/local/lib/R/etc/Renviron 56 | 57 | # copy the modified .Rprofile template to the renv cache 58 | COPY library-scripts/.Rprofile-vscode /renv/.Rprofile-vscode 59 | 60 | # [Optional] Uncomment this section to add addtional system dependencies needed for project 61 | 62 | # [Optional] Uncomment this section to install additional OS packages. 63 | RUN apt-get update \ 64 | && export DEBIAN_FRONTEND=noninteractive \ 65 | && apt-get -y install --no-install-recommends \ 66 | fonts-liberation \ 67 | gconf-service \ 68 | libxshmfence1 \ 69 | libasound2 \ 70 | libatk1.0-0 \ 71 | libcairo2 \ 72 | libcups2 \ 73 | libfontconfig1 \ 74 | libgbm-dev \ 75 | libgdk-pixbuf2.0-0 \ 76 | libgtk-3-0 \ 77 | libicu-dev \ 78 | libjpeg-dev \ 79 | libnspr4 \ 80 | libnss3 \ 81 | libpango-1.0-0 \ 82 | libpangocairo-1.0-0 \ 83 | libpng-dev \ 84 | libx11-6 \ 85 | libx11-xcb1 \ 86 | libxcb1 \ 87 | libxcomposite1 \ 88 | libxcursor1 \ 89 | libxdamage1 \ 90 | libxext6 \ 91 | libxfixes3 \ 92 | libxi6 \ 93 | libxrandr2 \ 94 | libxrender1 \ 95 | libxss1 \ 96 | libxtst6 \ 97 | xdg-utils 98 | 99 | RUN curl -sL https://deb.nodesource.com/setup_16.x -o /tmp/nodesource_setup.sh 100 | RUN bash /tmp/nodesource_setup.sh 101 | RUN apt-get update \ 102 | && export DEBIAN_FRONTEND=noninteractive \ 103 | && apt-get -y install --no-install-recommends nodejs 104 | 105 | RUN npm install puppeteer -g 106 | 107 | # [Optional] Uncomment this section to add Hugo to the container 108 | # Customize version number as appropriate 109 | #RUN curl -L https://github.com/gohugoio/hugo/releases/download/v0.87.0/hugo_extended_0.87.0_Linux-64bit.deb -o /tmp/hugo.deb 110 | #RUN apt-get -y install ./tmp/hugo.deb 111 | 112 | # [Optional] Uncomment this section to add addtional system dependencies needed for project 113 | # RUN apt-get update \ 114 | # && export DEBIAN_FRONTEND=noninteractive \ 115 | # && apt-get -y install --no-install-recommends ---packages list---- 116 | 117 | # [Optional] Uncomment this section for linking a local pusleaudio sound system on a linux host 118 | # to the container. 119 | RUN apt-get update \ 120 | && export DEBIAN_FRONTEND=noninteractive \ 121 | && apt-get -y install --no-install-recommends libpulse0 libasound2 libasound2-plugins pulseaudio-utils 122 | 123 | # install obs-cli (Go version) 124 | # https://github.com/muesli/obs-cli/releases 125 | ADD https://github.com/muesli/obs-cli/releases/download/v0.4.0/obs-cli_0.4.0_linux_x86_64.tar.gz /obs-cli-linux.tar.gz 126 | RUN tar -zxvf obs-cli-linux.tar.gz 127 | RUN cp /obs-cli /usr/local/bin/obs-cli 128 | RUN chmod 755 /usr/local/bin/obs-cli 129 | 130 | # install obs-cli (Javascript version) 131 | # https://github.com/leafac/obs-cli/releases 132 | ADD https://github.com/leafac/obs-cli/releases/download/v2.2.3/obs-cli-linux /usr/local/bin/obs-cli-js 133 | RUN chmod 755 /usr/local/bin/obs-cli-js 134 | 135 | # [Optional] Set the default user. Omit if you want to keep the default as root. 136 | USER $USERNAME 137 | 138 | -------------------------------------------------------------------------------- /vignettes/tidying_calendar.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "tidying_calendar" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{tidying_calendar} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | ```{r, include = FALSE} 11 | library(toastui) 12 | library(calendar) 13 | library(caldav) 14 | library(golem) 15 | library(dplyr) 16 | library(tidyr) 17 | library(clock) 18 | 19 | knitr::opts_chunk$set( 20 | collapse = TRUE, 21 | comment = "#>" 22 | ) 23 | ``` 24 | 25 | # Tidying up calendar metadata 26 | 27 | In this vignette, we will explore how to tidy up the raw import of a calendar from the backend server so it meets the requirements for the `{toastui}` calendar html widget. 28 | 29 | ## Required Format 30 | 31 | First we will load the built-in calendar data frame provided by `{toastui}` 32 | 33 | ```{r toastcal} 34 | toast_df <- toastui::cal_demo_data("month") 35 | dplyr::glimpse(toast_df) 36 | ``` 37 | 38 | There isn't substantial documentation on the requirements, but my best guess is the following columns are required: 39 | 40 | * `calendarId`: numeric id 41 | * `title`: Event title 42 | * `body`: Event description 43 | * `recurrenceRole`: If the event is a single occurrance, this is set to `NA`, otherwise for a weekly event it should be `Every week`. I'm not sure what the other valid values would be 44 | * `start`: Event start time. Format is `YYYY-MM-DD XX:YY:ZZ` 45 | * `end`: Event end time. Format is `YYYY-MM-DD XX:YY:ZZ` 46 | * `category`: Either `allday` for an all-day event, or `time` for a scheduled event not lasting the entire day 47 | * `location`: Location of event. If not needed, it can be `NA` 48 | * `bgColor`: Hex color for the event boc 49 | * `color`: Hex color of font? 50 | * `borderColor`: Hex color of border for the event box 51 | 52 | ## Raw import 53 | 54 | Next we will import the raw ical calendar from the server using custom functions that wrap the `{calendar}` and `{caldav}` packages 55 | 56 | ```{r rawimport} 57 | source("R/mod_cal_viewer_fct_helpers.R") 58 | source("R/app_config.R") 59 | cal_slug <- "data-science-streams" 60 | cal_base_url <- "https://nextcloud.r-podcastdev.link/remote.php/dav/calendars/shinycal" 61 | raw_df <- import_cal(cal_slug = cal_slug, cal_base_url = cal_base_url) %>% 62 | mutate(uid = UID) 63 | ``` 64 | 65 | ```{r rawexplore} 66 | dplyr::glimpse(raw_df) 67 | ``` 68 | 69 | We see that there is a fair bit of cleanup to do. A summary of issues that need to be solved: 70 | 71 | * There are multiple pairs of event start and end times, one pair for each time zone used when making events in the calendar. We need a way to keep only the pair with non-missing data and record the time zone used 72 | * The timestamp format is much different. An example is `20210514T210000` 73 | * `RRULE` looks similar in spirit to `recurrenceRole` but the values are different. Example is `FREQ=WEEKLY;BYDAY=WE;UNTIL=20210824T220000Z` and `FREQ=WEEKLY;BYDAY=TH` 74 | * Unclear what `SEQUENCE` does. The values are integers in character format. 75 | 76 | ## Let the tidying begin 77 | 78 | We will first tackle the multiple start and end times issue. I think the following approach should work: 79 | 80 | 1. Grab all columns that begin with `DTSTART` or `DTEND` 81 | 1. Create a unique ID column for row index to server as a unique identifier 82 | 1. Transpose to a long format 83 | 1. Strip out the time zone information from the time values and make them a separate column. It looks like we should be able to split by `;` 84 | 1. Make the data wide again with column names `id`, `start`, `end`, and `timezone` 85 | 86 | ```{r dttyding} 87 | dt_df <- raw_df %>% 88 | select(uid, starts_with("DTSTART"), starts_with("DTEND")) %>% 89 | tidyr::pivot_longer(!uid, names_to = c("dt_type", "timezone"), names_sep = ";", values_to = "time") %>% 90 | filter(!is.na(time)) %>% 91 | tidyr::pivot_wider(names_from = c(dt_type), values_from = time) 92 | #mutate(timezone = stringr::str_remove_all(timezone, "TZID=")) 93 | 94 | ``` 95 | 96 | That worked fairly well! Now we need to conform the start and end column values to match the string format used in `{toastui}`. Recall the raw and desired formats: 97 | 98 | **Raw**: `"20210514T210000"` 99 | **Final**: `"2021-05-14 21:00:00"` 100 | 101 | We will use the `{clock}` package to parse the raw formats (while incorporating time zones) and then get the desired formatting 102 | 103 | ```{r clock_exp} 104 | # experiment with clock package 105 | x <- "20210514T210000" 106 | x_zone <- "Europe/Zurich" 107 | 108 | y <- date_time_parse(x, zone = x_zone, format = "%Y%m%dT%H%M%S") %>% 109 | as_naive_time() %>% 110 | as_zoned_time(., x_zone) 111 | y 112 | 113 | # the character version 114 | #as.character(y) 115 | ``` 116 | 117 | Looks great! Now let's apply that to the processed data frame 118 | 119 | ```{r apply_date_format} 120 | time_parser <- function(x, zone, format = "%Y%m%dT%H%M%S", convert_to_char = FALSE) { 121 | x <- clock::date_time_parse(x, zone, format = format) %>% 122 | as_naive_time() %>% 123 | as_zoned_time(., zone) 124 | 125 | if (convert_to_char) { 126 | x <- as.character(x) 127 | } 128 | return(x) 129 | } 130 | 131 | dt_df2 <- dt_df %>% 132 | mutate( 133 | timezone = stringr::str_remove_all(timezone, "TZID="), 134 | start_clock = purrr::map2(DTSTART, timezone, ~time_parser(.x, .y)), 135 | #start = as.character(start_clock), 136 | end_clock = purrr::map2(DTEND, timezone, ~time_parser(.x, .y)), 137 | #end = as.character(end_clock) 138 | ) %>% 139 | select(uid, start_clock, end_clock, timezone) %>% 140 | tidyr::unnest(cols = c(start_clock, end_clock)) 141 | 142 | dplyr::glimpse(dt_df2) 143 | ``` 144 | 145 | Now we tackle the recurrence rule coding 146 | 147 | ```{r recurrance} 148 | rec_df <- raw_df %>% 149 | mutate(recurrenceRule = case_when( 150 | stringr::str_detect(RRULE, "FREQ=WEEKLY") ~ "Every week", 151 | TRUE ~ RRULE 152 | )) %>% 153 | select(uid, recurrenceRule) 154 | 155 | ``` 156 | 157 | I think we have what we need, so let's put this all together 158 | 159 | ```{r final_polish} 160 | final_df <- raw_df %>% 161 | select(uid, title = SUMMARY, location = LOCATION) %>% 162 | left_join(dt_df2, by = "uid") %>% 163 | left_join(rec_df, by = "uid") 164 | ``` 165 | 166 | Now let's see what happens when we view the times according to a particular time zone 167 | 168 | ```{r timeplay} 169 | final_df2 <- final_df %>% 170 | mutate(start_clock = zoned_time_set_zone(start_clock, "Europe/Zurich"), 171 | end_clock = zoned_time_set_zone(end_clock, "Europe/Zurich")) 172 | ``` 173 | 174 | Let's give the backend function a try 175 | 176 | ```{r backendfnct} 177 | source("R/mod_cal_viewer_fct_helpers.R") 178 | df8 <- process_cal(raw_df) 179 | ``` -------------------------------------------------------------------------------- /.devcontainer/library-scripts/common-debian.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | #------------------------------------------------------------------------------------------------------------- 3 | # Copyright (c) Microsoft Corporation. All rights reserved. 4 | # Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. 5 | #------------------------------------------------------------------------------------------------------------- 6 | 7 | # Syntax: ./common-debian.sh [install zsh flag] [username] [user UID] [user GID] [upgrade packages flag] 8 | 9 | INSTALL_ZSH=${1:-"true"} 10 | INSTALL_FISH=${2:-"true"} 11 | USERNAME=${3:-"vscode"} 12 | USER_UID=${4:-1000} 13 | USER_GID=${5:-1000} 14 | UPGRADE_PACKAGES=${6:-"true"} 15 | 16 | set -e 17 | 18 | if [ "$(id -u)" -ne 0 ]; then 19 | echo -e 'Script must be run a root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' 20 | exit 1 21 | fi 22 | 23 | # Treat a user name of "none" as root 24 | if [ "${USERNAME}" = "none" ] || [ "${USERNAME}" = "root" ]; then 25 | USERNAME=root 26 | USER_UID=0 27 | USER_GID=0 28 | fi 29 | 30 | # Load markers to see which steps have already run 31 | MARKER_FILE="/usr/local/etc/vscode-dev-containers/common" 32 | if [ -f "${MARKER_FILE}" ]; then 33 | echo "Marker file found:" 34 | cat "${MARKER_FILE}" 35 | source "${MARKER_FILE}" 36 | fi 37 | 38 | # Ensure apt is in non-interactive to avoid prompts 39 | export DEBIAN_FRONTEND=noninteractive 40 | 41 | # Function to call apt-get if needed 42 | apt-get-update-if-needed() 43 | { 44 | if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then 45 | echo "Running apt-get update..." 46 | apt-get update 47 | else 48 | echo "Skipping apt-get update." 49 | fi 50 | } 51 | 52 | # Run install apt-utils to avoid debconf warning then verify presence of other common developer tools and dependencies 53 | if [ "${PACKAGES_ALREADY_INSTALLED}" != "true" ]; then 54 | apt-get-update-if-needed 55 | 56 | PACKAGE_LIST="apt-utils \ 57 | git \ 58 | openssh-client \ 59 | less \ 60 | iproute2 \ 61 | procps \ 62 | curl \ 63 | wget \ 64 | unzip \ 65 | nano \ 66 | jq \ 67 | lsb-release \ 68 | ca-certificates \ 69 | apt-transport-https \ 70 | dialog \ 71 | gnupg2 \ 72 | libc6 \ 73 | libgcc1 \ 74 | libgssapi-krb5-2 \ 75 | libicu[0-9][0-9] \ 76 | liblttng-ust0 \ 77 | libstdc++6 \ 78 | zlib1g \ 79 | locales \ 80 | sudo" 81 | 82 | # Install libssl1.1 if available 83 | if [[ ! -z $(apt-cache --names-only search ^libssl1.1$) ]]; then 84 | PACKAGE_LIST="${PACKAGE_LIST} libssl1.1" 85 | fi 86 | 87 | # Install appropriate version of libssl1.0.x if available 88 | LIBSSL=$(dpkg-query -f '${db:Status-Abbrev}\t${binary:Package}\n' -W 'libssl1\.0\.?' 2>&1 || echo '') 89 | if [ "$(echo "$LIBSSL" | grep -o 'libssl1\.0\.[0-9]:' | uniq | sort | wc -l)" -eq 0 ]; then 90 | if [[ ! -z $(apt-cache --names-only search ^libssl1.0.2$) ]]; then 91 | # Debian 9 92 | PACKAGE_LIST="${PACKAGE_LIST} libssl1.0.2" 93 | elif [[ ! -z $(apt-cache --names-only search ^libssl1.0.0$) ]]; then 94 | # Ubuntu 18.04, 16.04, earlier 95 | PACKAGE_LIST="${PACKAGE_LIST} libssl1.0.0" 96 | fi 97 | fi 98 | 99 | echo "Packages to verify are installed: ${PACKAGE_LIST}" 100 | apt-get -y install --no-install-recommends ${PACKAGE_LIST} 2> >( grep -v 'debconf: delaying package configuration, since apt-utils is not installed' >&2 ) 101 | 102 | PACKAGES_ALREADY_INSTALLED="true" 103 | fi 104 | 105 | # Get to latest versions of all packages 106 | if [ "${UPGRADE_PACKAGES}" = "true" ]; then 107 | apt-get-update-if-needed 108 | apt-get -y upgrade --no-install-recommends 109 | apt-get autoremove -y 110 | fi 111 | 112 | # Ensure at least the en_US.UTF-8 UTF-8 locale is available. 113 | # Common need for both applications and things like the agnoster ZSH theme. 114 | if [ "${LOCALE_ALREADY_SET}" != "true" ]; then 115 | echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen 116 | locale-gen 117 | LOCALE_ALREADY_SET="true" 118 | fi 119 | 120 | # Create or update a non-root user to match UID/GID - see https://aka.ms/vscode-remote/containers/non-root-user. 121 | if id -u $USERNAME > /dev/null 2>&1; then 122 | # User exists, update if needed 123 | if [ "$USER_GID" != "$(id -G $USERNAME)" ]; then 124 | groupmod --gid $USER_GID $USERNAME 125 | usermod --gid $USER_GID $USERNAME 126 | fi 127 | if [ "$USER_UID" != "$(id -u $USERNAME)" ]; then 128 | usermod --uid $USER_UID $USERNAME 129 | fi 130 | else 131 | # Create user 132 | groupadd --gid $USER_GID $USERNAME 133 | useradd -s /bin/bash --uid $USER_UID --gid $USER_GID -m $USERNAME 134 | fi 135 | 136 | # Add add sudo support for non-root user 137 | if [ "${EXISTING_NON_ROOT_USER}" != "${USERNAME}" ]; then 138 | echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME 139 | chmod 0440 /etc/sudoers.d/$USERNAME 140 | EXISTING_NON_ROOT_USER="${USERNAME}" 141 | fi 142 | 143 | # Ensure ~/.local/bin is in the PATH for root and non-root users for bash. (zsh is later) 144 | if [ "${DOT_LOCAL_ALREADY_ADDED}" != "true" ]; then 145 | echo "export PATH=\$PATH:\$HOME/.local/bin" | tee -a /root/.bashrc >> /home/$USERNAME/.bashrc 146 | chown $USER_UID:$USER_GID /home/$USERNAME/.bashrc 147 | DOT_LOCAL_ALREADY_ADDED="true" 148 | fi 149 | 150 | # Optionally install and configure zsh 151 | if [ "${INSTALL_ZSH}" = "true" ] && [ ! -d "/root/.oh-my-zsh" ] && [ "${ZSH_ALREADY_INSTALLED}" != "true" ]; then 152 | apt-get-update-if-needed 153 | apt-get install -y zsh 154 | curl -fsSLo- https://raw.githubusercontent.com/robbyrussell/oh-my-zsh/master/tools/install.sh | bash 2>&1 155 | echo "export PATH=\$PATH:\$HOME/.local/bin" >> /root/.zshrc 156 | if [ "${USERNAME}" != "root" ]; then 157 | cp -fR /root/.oh-my-zsh /home/$USERNAME 158 | cp -f /root/.zshrc /home/$USERNAME 159 | sed -i -e "s/\/root\/.oh-my-zsh/\/home\/$USERNAME\/.oh-my-zsh/g" /home/$USERNAME/.zshrc 160 | chown -R $USER_UID:$USER_GID /home/$USERNAME/.oh-my-zsh /home/$USERNAME/.zshrc 161 | fi 162 | ZSH_ALREADY_INSTALLED="true" 163 | fi 164 | 165 | # Optionally install and configure fish 166 | if [ "${INSTALL_FISH}" = "true" ] && [ "${FISH_ALREADY_INSTALLED}" != "true" ]; then 167 | apt-get-update-if-needed 168 | apt-get install -y fish 169 | #curl -fsSLo- https://raw.githubusercontent.com/robbyrussell/oh-my-zsh/master/tools/install.sh | bash 2>&1 170 | #echo "export PATH=\$PATH:\$HOME/.local/bin" >> /root/.zshrc 171 | #if [ "${USERNAME}" != "root" ]; then 172 | # cp -fR /root/.oh-my-zsh /home/$USERNAME 173 | # cp -f /root/.zshrc /home/$USERNAME 174 | # sed -i -e "s/\/root\/.oh-my-zsh/\/home\/$USERNAME\/.oh-my-zsh/g" /home/$USERNAME/.zshrc 175 | # chown -R $USER_UID:$USER_GID /home/$USERNAME/.oh-my-zsh /home/$USERNAME/.zshrc 176 | #fi 177 | FISH_ALREADY_INSTALLED="true" 178 | fi 179 | 180 | # Write marker file 181 | mkdir -p "$(dirname "${MARKER_FILE}")" 182 | echo -e "\ 183 | PACKAGES_ALREADY_INSTALLED=${PACKAGES_ALREADY_INSTALLED}\n\ 184 | LOCALE_ALREADY_SET=${LOCALE_ALREADY_SET}\n\ 185 | EXISTING_NON_ROOT_USER=${EXISTING_NON_ROOT_USER}\n\ 186 | DOT_LOCAL_ALREADY_ADDED=${DOT_LOCAL_ALREADY_ADDED}\n\ 187 | ZSH_ALREADY_INSTALLED=${ZSH_ALREADY_INSTALLED}\n\ 188 | FISH_ALREADY_INSTALLED=${FISH_ALREADY_INSTALLED}" > "${MARKER_FILE}" 189 | -------------------------------------------------------------------------------- /vignettes/streamer_info.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "streamer_info" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{streamer_info} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | ```{r, include = FALSE} 11 | knitr::opts_chunk$set( 12 | collapse = TRUE, 13 | comment = "#>" 14 | ) 15 | ``` 16 | 17 | ## Obtaining metadata associated with streamers 18 | 19 | The goal of this vignette is to illustrate how we can get interesting metadata associated with each streamer via the Twitch and YouTube APIs. 20 | 21 | ## Setup 22 | 23 | ```{r setup, include = FALSE} 24 | library(twitchr) 25 | library(httr) 26 | library(calendar) 27 | library(caldav) 28 | library(dplyr) 29 | library(yaml) 30 | library(clock) 31 | 32 | 33 | readRenviron(".Renviron") 34 | ``` 35 | 36 | ## Twitch streamer metadata 37 | 38 | ```{r twitchauth} 39 | # set up authentication via environment variables 40 | twitch_auth() 41 | ``` 42 | 43 | Authentication is successful! Now let's experiment with the functions in the `twitchr` package, as well as custom explorations of obtaining a streamer's calendar via the Twitch API directly, see docs [here](https://dev.twitch.tv/docs/api/reference/#get-channel-icalendar) 44 | 45 | 46 | ## Obtain streamer YAML file data 47 | 48 | ```{r yamlparsing} 49 | yml_file <- "data-raw/streamers.yml" 50 | yml_data <- yaml::read_yaml(file = yml_file) 51 | 52 | # functions are in separate script now 53 | source("R/mod_cal_viewer_fct_helpers.R") 54 | 55 | streamer_data <- tibble(yml_data$streamers) %>% 56 | tidyr::unnest_wider(1) %>% 57 | filter(platform == "twitch") %>% 58 | mutate(id = purrr::map_chr(user_id, ~get_twitch_id(user_name = .x))) %>% 59 | mutate(schedule_data = purrr::map(id, ~get_twitch_schedule(.x)), 60 | video_id = purrr::map_chr(id, ~get_twitch_videos(.x))) 61 | ``` 62 | ### Import calendar 63 | 64 | ```{r caltry} 65 | 66 | user <- get_users(login = "aftonsteps") 67 | 68 | r <- httr::GET("https://api.twitch.tv/helix/schedule/icalendar", query = list(broadcaster_id = user$id)) 69 | status_code(r) 70 | x <- content(r, "text") 71 | 72 | res <- withr::with_tempfile("tf", { 73 | cat(x, file = tf) 74 | ic_read(tf) 75 | }) 76 | ``` 77 | 78 | ## Import last two weeks of video data in case schedule is not available 79 | 80 | ```{r twitchvideos} 81 | # functions are in separate script now 82 | source("R/mod_cal_viewer_fct_helpers.R") 83 | 84 | user <- get_users(login = "nickwan_datasci") 85 | 86 | #twitch_clips <- twitchr::get_all_clips(broadcaster_id = user$id) 87 | 88 | r <- httr::GET("https://api.twitch.tv/helix/videos", query = list(user_id = user$id, period = "week")) 89 | status_code(r) 90 | 91 | 92 | res <- httr::content(r, "parsed") %>% 93 | purrr::pluck("data") %>% 94 | tibble::tibble() %>% 95 | tidyr::unnest_wider(1) 96 | 97 | current_weekday <- date_now("America/New_York") %>% 98 | as_year_month_weekday() %>% 99 | get_day() 100 | 101 | prev_week_date <- date_now("America/New_York") %>% 102 | clock::date_shift(target = weekday(current_weekday), which = "previous", boundary = "advance") 103 | 104 | current_sunday <- date_now("America/New_York") %>% 105 | clock::date_shift(target = weekday(clock_weekdays$sunday), which = "previous") 106 | 107 | shift_up_week <- function(x, convert = TRUE) { 108 | if (!convert) { 109 | return(x) 110 | } else { 111 | # get current day of week from supplied date 112 | x_weekday <- clock::as_year_month_weekday(x) %>% clock::get_day() 113 | 114 | res <- clock::date_shift(x, target = clock::weekday(x_weekday), which = "next", boundary = "advance") 115 | 116 | return(res) 117 | } 118 | } 119 | 120 | res_final <- res %>% 121 | mutate(start = purrr::map(created_at, ~time_parser(.x, convert_to_char = FALSE))) %>% 122 | mutate(start = purrr::map(start, ~clock::as_date_time(.x, zone = "America/New_York"))) %>% 123 | mutate(duration2 = purrr::map_dbl(duration, ~parse_duration(.x, "seconds"))) %>% 124 | tidyr::unnest(cols = c(start)) %>% 125 | mutate(start = purrr::map(start, ~round_time(.x))) %>% 126 | mutate(end = purrr::map2(start, duration2, ~compute_end_clock(.x, .y))) %>% 127 | mutate(category = "time", 128 | recurrenceRule = "Every week", 129 | start_time = NA, 130 | end_time = NA) %>% 131 | dplyr::select(start_time, start, end_time, end, title, category, recurrenceRule) %>% 132 | tidyr::unnest(cols = c(start, end)) %>% 133 | filter(start > prev_week_date) %>% 134 | mutate(before_week_ind = start < current_sunday) %>% 135 | mutate(start = purrr::map2(start, before_week_ind, ~shift_up_week(.x, .y))) %>% 136 | mutate(end = purrr::map2(end, before_week_ind, ~shift_up_week(.x, .y))) %>% 137 | mutate(start = as.character(start), end = as.character(end)) 138 | 139 | # experiment with time shifting 140 | # ideas 141 | # - based on current day, find the first day of the current week 142 | # - if the start date time is on a date before the first day of the week, then shift the date up. Otherwise, leave as it is 143 | 144 | test_weekdate <- clock::as_year_month_weekday(res_final$start[1]) 145 | test_date <- clock::as_year_month_day(res_final$start[1]) 146 | get_week(test_date) 147 | 148 | sunday <- clock::weekday(clock_weekdays$sunday) 149 | 150 | bench_date <- clock::date_shift(res_final$start[1], sunday, which = "previous") 151 | 152 | clock::date_shift(res_final$start[1], sunday, which = "previous") 153 | 154 | res_final$start[1] < bench_date 155 | 156 | clock::weekday(clock::date_now(zone = "America/New_York")) 157 | 158 | clock::date_shift(test_date, target = sunday, which = "previous") 159 | 160 | # experiment with rounding 161 | test_time <- clock::as_date_time(twitch_vids$start_clock[[1]], "America/New_York") 162 | 163 | # https://stackoverflow.com/questions/27397332/find-round-the-time-to-nearest-half-an-hour-from-a-single-file 164 | test_time2 <- as.POSIXlt(test_time) 165 | test_time2$min 166 | 167 | test_time2$min <- round(test_time2$min / 30) * 30 168 | test_time2 169 | 170 | test_time2$sec 171 | test_time2$sec <- floor(test_time2$sec / 60) 172 | test_time2 173 | 174 | as.POSIXct(test_time2) 175 | 176 | 177 | clock::date_round(test_time, "hour") 178 | 179 | 180 | twitch_vids$end_clock[[1]] 181 | twitch_vids$start_clock[[1]] 182 | clock::add_seconds(twitch_vids$end_clock[[1]], -30) 183 | View(twitch_vids) 184 | 185 | 186 | 187 | 188 | dur_parsed <- stringr::str_match_all(test_duration, dur_regex) 189 | dur_parsed 190 | 191 | r <- httr::GET("https://api.twitch.tv/helix/streams", query = list(user_id = user$id)) 192 | status_code(r) 193 | 194 | res2 <- content(r, "parsed") 195 | twitch_streams <- res2 196 | 197 | ``` 198 | 199 | 200 | ### Import stream schedule 201 | 202 | ```{r schedule} 203 | r <- httr::GET("https://api.twitch.tv/helix/schedule", query = list(broadcaster_id = user$id)) 204 | status_code(r) 205 | 206 | res2 <- content(r, "parsed") 207 | 208 | res3 <- get_schedule(broadcaster_id = user$id) %>% 209 | .[["data"]] %>% 210 | tibble() 211 | 212 | res3 213 | ``` 214 | 215 | ### Import additional metadata 216 | 217 | ```{r other-meta} 218 | videos <- get_videos(user_id = user$id, first = 100) 219 | 220 | videos_play <- videos$data %>% 221 | tibble() %>% 222 | mutate(video_id = purrr::map_chr(url, ~{ 223 | tmp <- stringr::str_split(.x, "/") 224 | n_items <- length(tmp[[1]]) 225 | res <- tmp[[1]][n_items] 226 | return(res) 227 | })) 228 | 229 | videos_df <- videos$data %>% 230 | mutate(video_id = purrr::map_chr(url, ~{ 231 | tmp <- stringr::str_split(.x, "/") 232 | n_items <- length(tmp[[1]]) 233 | res <- tmp[[1]][n_items] 234 | return(res) 235 | })) 236 | 237 | # video id 1060733999 238 | followers <- get_follows(to_id = user$id, first = 100) 239 | followers_df <- followers$data 240 | 241 | ``` 242 | 243 | ## YouTube stream metadata 244 | 245 | ```{r youtube, eval=FALSE} 246 | channel_id <- "UCeiiqmVK07qhY-wvg3IZiZQ" 247 | part <- "liveStreamingDetails" 248 | part <- "snippet,contentDetails" 249 | part <- "snippet,contentDetails,liveStreamingDetails" 250 | #base_url <- "https://www.googleapis.com/youtube/v3/channels" 251 | base_url <- "https://www.googleapis.com/youtube/v3/liveBroadcasts" 252 | base_url <- "https://www.googleapis.com/youtube/v3/videos" 253 | yt_request <- httr::GET(base_url, 254 | query = list(key = Sys.getenv("YOUTUBE_API_KEY"), 255 | id = channel_id, 256 | part = part)) 257 | 258 | status_code(yt_request) 259 | 260 | res3 <- content(yt_request, "parsed") 261 | ``` -------------------------------------------------------------------------------- /dev/r_code/wimpys-world-of-streamers-2021-06-08.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | CALSCALE:GREGORIAN 4 | PRODID:-//SabreDAV//SabreDAV//EN 5 | X-WR-CALNAME:Wimpy's World of Streamers 6 | X-APPLE-CALENDAR-COLOR:#248EB5 7 | REFRESH-INTERVAL;VALUE=DURATION:PT4H 8 | X-PUBLISHED-TTL:PT4H 9 | BEGIN:VTIMEZONE 10 | TZID:Europe/Zurich 11 | BEGIN:DAYLIGHT 12 | TZOFFSETFROM:+0100 13 | TZOFFSETTO:+0200 14 | TZNAME:CEST 15 | DTSTART:19700329T020000 16 | RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU 17 | END:DAYLIGHT 18 | BEGIN:STANDARD 19 | TZOFFSETFROM:+0200 20 | TZOFFSETTO:+0100 21 | TZNAME:CET 22 | DTSTART:19701025T030000 23 | RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU 24 | END:STANDARD 25 | END:VTIMEZONE 26 | BEGIN:VTIMEZONE 27 | TZID:Europe/Ljubljana 28 | BEGIN:DAYLIGHT 29 | TZOFFSETFROM:+0100 30 | TZOFFSETTO:+0200 31 | TZNAME:CEST 32 | DTSTART:19700329T020000 33 | RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU 34 | END:DAYLIGHT 35 | BEGIN:STANDARD 36 | TZOFFSETFROM:+0200 37 | TZOFFSETTO:+0100 38 | TZNAME:CET 39 | DTSTART:19701025T030000 40 | RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU 41 | END:STANDARD 42 | END:VTIMEZONE 43 | BEGIN:VTIMEZONE 44 | TZID:America/New_York 45 | BEGIN:DAYLIGHT 46 | TZOFFSETFROM:-0500 47 | TZOFFSETTO:-0400 48 | TZNAME:EDT 49 | DTSTART:19700308T020000 50 | RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU 51 | END:DAYLIGHT 52 | BEGIN:STANDARD 53 | TZOFFSETFROM:-0400 54 | TZOFFSETTO:-0500 55 | TZNAME:EST 56 | DTSTART:19701101T020000 57 | RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU 58 | END:STANDARD 59 | END:VTIMEZONE 60 | BEGIN:VTIMEZONE 61 | TZID:America/Indiana/Indianapolis 62 | BEGIN:DAYLIGHT 63 | TZOFFSETFROM:-0500 64 | TZOFFSETTO:-0400 65 | TZNAME:EDT 66 | DTSTART:19700308T020000 67 | RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU 68 | END:DAYLIGHT 69 | BEGIN:STANDARD 70 | TZOFFSETFROM:-0400 71 | TZOFFSETTO:-0500 72 | TZNAME:EST 73 | DTSTART:19701101T020000 74 | RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU 75 | END:STANDARD 76 | END:VTIMEZONE 77 | BEGIN:VEVENT 78 | CREATED:20210512T060448Z 79 | DTSTAMP:20210512T060706Z 80 | LAST-MODIFIED:20210512T060706Z 81 | SEQUENCE:2 82 | UID:2f4bbb1a-5f0f-43d3-a3c4-b01a8d889a84 83 | DTSTART;TZID=Europe/Zurich:20210514T210000 84 | DTEND;TZID=Europe/Zurich:20210514T220000 85 | SUMMARY:StreamBerry : a DIY StreamDeck\, part 1 86 | LOCATION:Twitch 87 | DESCRIPTION:Let's do some Python magic\, and make a client program connect 88 | to a server program with zero configuration. 89 | END:VEVENT 90 | BEGIN:VEVENT 91 | CREATED:20210524T203932Z 92 | DTSTAMP:20210524T204054Z 93 | LAST-MODIFIED:20210524T204054Z 94 | SEQUENCE:3 95 | UID:397edb19-e6f6-46cf-9640-5b8932729fad 96 | DTSTART;TZID=Europe/Ljubljana:20210529T223000 97 | DTEND;TZID=Europe/Ljubljana:20210530T003000 98 | SUMMARY:Bigpod's Tries to implement all he learned about python in actual p 99 | rogram 100 | END:VEVENT 101 | BEGIN:VEVENT 102 | CREATED:20210524T204217Z 103 | DTSTAMP:20210524T204305Z 104 | LAST-MODIFIED:20210524T204305Z 105 | SEQUENCE:2 106 | UID:50e7645d-2165-4434-bce0-7b38e64c8bbf 107 | DTSTART;TZID=Europe/Ljubljana:20210602T223000 108 | DTEND;TZID=Europe/Ljubljana:20210603T003000 109 | SUMMARY:Bigpod's stream about repositoryManagerNet 110 | END:VEVENT 111 | BEGIN:VEVENT 112 | CREATED:20210529T161306Z 113 | DTSTAMP:20210529T161432Z 114 | LAST-MODIFIED:20210529T161432Z 115 | SEQUENCE:2 116 | UID:9824ae8c-357d-4a33-8ef8-2023621e972e 117 | DTSTART;TZID=Europe/Ljubljana:20210609T223000 118 | DTEND;TZID=Europe/Ljubljana:20210610T003000 119 | RRULE:FREQ=WEEKLY;BYDAY=WE;UNTIL=20210824T220000Z 120 | SUMMARY:bigpods stream titles TBD 121 | END:VEVENT 122 | BEGIN:VEVENT 123 | CREATED:20210524T195012Z 124 | DTSTAMP:20210524T195236Z 125 | LAST-MODIFIED:20210524T195236Z 126 | SEQUENCE:3 127 | UID:f27c3ef5-8f33-4893-b612-22e6c5a32e21 128 | DTSTART;TZID=Europe/Zurich:20210526T210000 129 | DTEND;TZID=Europe/Zurich:20210526T230000 130 | SUMMARY:Tea\, Earl Grey\, Hot ! - Live recording 131 | LOCATION:YouTube + Twitch 132 | DESCRIPTION:We're going to Twitch for the 1st time ! 133 | END:VEVENT 134 | BEGIN:VEVENT 135 | CREATED:20210510T195914Z 136 | DTSTAMP:20210510T200001Z 137 | LAST-MODIFIED:20210510T200001Z 138 | SEQUENCE:3 139 | UID:9a6c1e51-d481-4293-b981-32b4f005b437 140 | DTSTART;TZID=America/New_York:20210510T163000 141 | DTEND;TZID=America/New_York:20210510T173000 142 | SUMMARY:Monica - Learning Factory Pt. 2 (Twitch) 143 | END:VEVENT 144 | BEGIN:VEVENT 145 | CREATED:20210520T195427Z 146 | DTSTAMP:20210520T195506Z 147 | LAST-MODIFIED:20210520T195506Z 148 | SEQUENCE:2 149 | UID:6d7008da-5969-4f55-b0e3-c7e1861401c0 150 | DTSTART;TZID=Europe/Ljubljana:20210522T223000 151 | DTEND;TZID=Europe/Ljubljana:20210523T003000 152 | SUMMARY:bigpod trying to learn Python on stream 153 | END:VEVENT 154 | BEGIN:VEVENT 155 | CREATED:20210524T195434Z 156 | DTSTAMP:20210524T195517Z 157 | LAST-MODIFIED:20210524T195517Z 158 | SEQUENCE:4 159 | UID:0172ff7b-44de-4836-b14b-7cebcc6f1835 160 | DTSTART;TZID=Europe/Zurich:20210527T220000 161 | DTEND;TZID=Europe/Zurich:20210527T230000 162 | SUMMARY:Software Boutique 2.0\, part 6 163 | LOCATION:YouTube 164 | END:VEVENT 165 | BEGIN:VEVENT 166 | CREATED:20210528T131756Z 167 | DTSTAMP:20210528T132939Z 168 | LAST-MODIFIED:20210528T132939Z 169 | SEQUENCE:6 170 | UID:7726ab4c-0f9c-4fde-839a-076830ea1435 171 | DTSTART;TZID=Europe/Zurich:20210603T220000 172 | DTEND;TZID=Europe/Zurich:20210603T230000 173 | SUMMARY:Software Boutique 2.0 174 | RRULE:FREQ=WEEKLY;BYDAY=TH 175 | LOCATION:YouTube 176 | EXDATE;TZID=Europe/Zurich:20210603T220000 177 | END:VEVENT 178 | BEGIN:VEVENT 179 | CREATED:20210524T132807Z 180 | DTSTAMP:20210524T132907Z 181 | LAST-MODIFIED:20210524T132907Z 182 | SEQUENCE:2 183 | UID:352ea3ae-982c-4b92-9271-3e5fdbfc85f6 184 | DTSTART;TZID=America/Indiana/Indianapolis:20210525T153000 185 | DTEND;TZID=America/Indiana/Indianapolis:20210525T163000 186 | SUMMARY:Using Docker Slim with R & Shiny containers (Eric and Martin) 187 | END:VEVENT 188 | BEGIN:VEVENT 189 | CREATED:20210424T210448Z 190 | DTSTAMP:20210428T200727Z 191 | LAST-MODIFIED:20210428T200727Z 192 | SEQUENCE:5 193 | UID:b146c8b0-6c21-4d0c-b00a-f018531e7c16 194 | DTSTART;TZID=Europe/Ljubljana:20210428T220000 195 | DTEND;TZID=Europe/Ljubljana:20210429T003000 196 | SUMMARY:Bigpod's stream about RepositoryManagerNet 197 | LOCATION:https://www.youtube.com/watch?v=aeDbsznAvKQ 198 | DESCRIPTION:https://www.youtube.com/watch?v=aeDbsznAvKQ 199 | END:VEVENT 200 | BEGIN:VEVENT 201 | CREATED:20210524T194904Z 202 | DTSTAMP:20210524T195254Z 203 | LAST-MODIFIED:20210524T195254Z 204 | SEQUENCE:3 205 | UID:4206ab89-925a-4a99-8868-56ff280ef96e 206 | DTSTART;TZID=Europe/Zurich:20210525T223000 207 | DTEND;TZID=Europe/Zurich:20210525T233000 208 | SUMMARY:How to use Glimpse to automate the creation of stream titles 209 | LOCATION:Twitch 210 | END:VEVENT 211 | BEGIN:VEVENT 212 | CREATED:20210423T201110Z 213 | DTSTAMP:20210423T201150Z 214 | LAST-MODIFIED:20210423T201150Z 215 | SEQUENCE:2 216 | UID:0a381960-39d1-4693-9e73-0624a53b8b1b 217 | DTSTART;TZID=Europe/Zurich:20210506T213000 218 | DTEND;TZID=Europe/Zurich:20210506T223000 219 | SUMMARY:Software Boutique 2.0\, part 3 220 | END:VEVENT 221 | BEGIN:VEVENT 222 | CREATED:20210423T224127Z 223 | DTSTAMP:20210423T224431Z 224 | LAST-MODIFIED:20210423T224431Z 225 | SEQUENCE:3 226 | UID:c0e5837f-28b6-4a24-ad94-bb684d0d857a 227 | DTSTART;TZID=America/New_York:20210426T163000 228 | DTEND;TZID=America/New_York:20210426T180000 229 | SUMMARY:Monica's Oh My Git Part 2 livestream 230 | LOCATION:https://www.twitch.tv/communiteatime 231 | DESCRIPTION:More shenanigans with learning Git through games! 232 | END:VEVENT 233 | BEGIN:VEVENT 234 | CREATED:20210524T195527Z 235 | DTSTAMP:20210524T195620Z 236 | LAST-MODIFIED:20210524T195620Z 237 | SEQUENCE:2 238 | UID:a72d3df6-be41-4c26-8eb6-12e389f9b6ad 239 | DTSTART;TZID=Europe/Zurich:20210529T210000 240 | DTEND;TZID=Europe/Zurich:20210529T220000 241 | SUMMARY:StreamBerry\, part 2 242 | END:VEVENT 243 | BEGIN:VEVENT 244 | CREATED:20210423T191740Z 245 | DTSTAMP:20210423T192027Z 246 | LAST-MODIFIED:20210423T192027Z 247 | SEQUENCE:3 248 | UID:f09058a5-8d12-454a-86e0-223c2081a798 249 | DTSTART;TZID=America/Indiana/Indianapolis:20210429T153000 250 | DTEND;TZID=America/Indiana/Indianapolis:20210429T163000 251 | SUMMARY:Eric's Intro to R livestream! 252 | DESCRIPTION:Streaming to Twitch: https://www.twitch.tv/rpodcast 253 | END:VEVENT 254 | BEGIN:VEVENT 255 | CREATED:20210510T075724Z 256 | DTSTAMP:20210510T195951Z 257 | LAST-MODIFIED:20210510T195951Z 258 | SEQUENCE:4 259 | UID:3b88e1b7-5860-4598-939e-b30bce3c9374 260 | DTSTART;TZID=Europe/Zurich:20210513T220000 261 | DTEND;TZID=Europe/Zurich:20210513T230000 262 | SUMMARY:Software Boutique 2.0\, part 4 263 | LOCATION:YouTube 264 | DESCRIPTION:Let's bring the curated apps in ! 265 | END:VEVENT 266 | BEGIN:VEVENT 267 | DTSTAMP:20210603T183630Z 268 | UID:e6bac299-4e26-47ba-84c9-18de61c96ff0 269 | SUMMARY:Eric's shiny dev series livestream on twitch 270 | DTSTART;TZID=America/New_York:20210608T153000 271 | DTEND;TZID=America/New_York:20210608T163000 272 | SEQUENCE:1 273 | LAST-MODIFIED:20210603T183630Z 274 | END:VEVENT 275 | END:VCALENDAR 276 | -------------------------------------------------------------------------------- /R/golem_utils_ui.R: -------------------------------------------------------------------------------- 1 | #' Turn an R list into an HTML list 2 | #' 3 | #' @param list An R list 4 | #' @param class a class for the list 5 | #' 6 | #' @return an HTML list 7 | #' @noRd 8 | #' 9 | #' @examples 10 | #' list_to_li(c("a","b")) 11 | #' 12 | #' @importFrom shiny tags tagAppendAttributes tagList 13 | list_to_li <- function(list, class = NULL){ 14 | if (is.null(class)){ 15 | tagList( 16 | lapply( 17 | list, 18 | tags$li 19 | ) 20 | ) 21 | } else { 22 | res <- lapply( 23 | list, 24 | tags$li 25 | ) 26 | res <- lapply( 27 | res, 28 | function(x) { 29 | tagAppendAttributes( 30 | x, 31 | class = class 32 | ) 33 | } 34 | ) 35 | tagList(res) 36 | } 37 | 38 | } 39 | #' Turn an R list into corresponding HTML paragraph tags 40 | #' 41 | #' @param list an R list 42 | #' @param class a class for the paragraph tags 43 | #' 44 | #' @return An HTML tag 45 | #' @noRd 46 | #' 47 | #' @examples 48 | #' list_to_p(c("This is the first paragraph", "this is the second paragraph")) 49 | #' 50 | #' @importFrom shiny tags tagAppendAttributes tagList 51 | #' 52 | list_to_p <- function(list, class = NULL){ 53 | if (is.null(class)){ 54 | tagList( 55 | lapply( 56 | list, 57 | tags$p 58 | ) 59 | ) 60 | } else { 61 | res <- lapply( 62 | list, 63 | tags$p 64 | ) 65 | res <- lapply( 66 | res, 67 | function(x) { 68 | tagAppendAttributes( 69 | x, 70 | class = class 71 | ) 72 | } 73 | ) 74 | tagList(res) 75 | } 76 | 77 | } 78 | 79 | #' @importFrom shiny tags tagAppendAttributes tagList 80 | named_to_li <- function(list, class = NULL){ 81 | if(is.null(class)){ 82 | res <- mapply( 83 | function(x, y){ 84 | tags$li( 85 | HTML( 86 | sprintf("%s: %s", y, x) 87 | ) 88 | ) 89 | }, 90 | list, 91 | names(list), 92 | SIMPLIFY = FALSE 93 | ) 94 | tagList(res) 95 | } else { 96 | res <- mapply( 97 | function(x, y){ 98 | tags$li( 99 | HTML( 100 | sprintf("%s: %s", y, x) 101 | ) 102 | ) 103 | }, 104 | list, 105 | names(list), 106 | SIMPLIFY = FALSE 107 | ) 108 | res <- lapply( 109 | res, 110 | function(x) { 111 | tagAppendAttributes( 112 | x, 113 | class = class 114 | ) 115 | } 116 | ) 117 | tagList(res) 118 | } 119 | } 120 | 121 | #' Remove a tag attribute 122 | #' 123 | #' @param tag the tag 124 | #' @param ... the attributes to remove 125 | #' 126 | #' @return a new tag 127 | #' @noRd 128 | #' 129 | #' @examples 130 | #' a <- shiny::tags$p(src = "plop", "pouet") 131 | #' tagRemoveAttributes(a, "src") 132 | tagRemoveAttributes <- function(tag, ...) { 133 | attrs <- as.character(list(...)) 134 | for (i in seq_along(attrs)) { 135 | tag$attribs[[ attrs[i] ]] <- NULL 136 | } 137 | tag 138 | } 139 | 140 | #' Hide or display a tag 141 | #' 142 | #' @param tag the tag 143 | #' 144 | #' @return a tag 145 | #' @noRd 146 | #' 147 | #' @examples 148 | #' ## Hide 149 | #' a <- shiny::tags$p(src = "plop", "pouet") 150 | #' undisplay(a) 151 | #' b <- shiny::actionButton("go_filter", "go") 152 | #' undisplay(b) 153 | #' 154 | #' @importFrom shiny tagList 155 | undisplay <- function(tag) { 156 | # if not already hidden 157 | if ( 158 | !is.null(tag$attribs$style) && 159 | !grepl("display:\\s+none", tag$attribs$style) 160 | ) { 161 | tag$attribs$style <- paste( 162 | "display: none;", 163 | tag$attribs$style 164 | ) 165 | } else { 166 | tag$attribs$style <- "display: none;" 167 | } 168 | tag 169 | } 170 | 171 | #' @importFrom shiny tagList 172 | display <- function(tag) { 173 | if ( 174 | !is.null(tag$attribs$style) && 175 | grepl("display:\\s+none", tag$attribs$style) 176 | ) { 177 | tag$attribs$style <- gsub( 178 | "(\\s)*display:(\\s)*none(\\s)*(;)*(\\s)*", 179 | "", 180 | tag$attribs$style 181 | ) 182 | } 183 | tag 184 | } 185 | 186 | #' Hide an elements by calling jquery hide on it 187 | #' 188 | #' @param id the id of the element to hide 189 | #' 190 | #' @noRd 191 | #' 192 | #' @importFrom shiny tags 193 | jq_hide <- function(id) { 194 | tags$script(sprintf("$('#%s').hide()", id)) 195 | } 196 | 197 | #' Add a red star at the end of the text 198 | #' 199 | #' Adds a red star at the end of the text 200 | #' (for example for indicating mandatory fields). 201 | #' 202 | #' @param text the HTLM text to put before the red star 203 | #' 204 | #' @return an html element 205 | #' @noRd 206 | #' 207 | #' @examples 208 | #' with_red_star("Enter your name here") 209 | #' 210 | #' @importFrom shiny tags HTML 211 | with_red_star <- function(text) { 212 | shiny::tags$span( 213 | HTML( 214 | paste0( 215 | text, 216 | shiny::tags$span( 217 | style = "color:red", "*" 218 | ) 219 | ) 220 | ) 221 | ) 222 | } 223 | 224 | 225 | 226 | #' Repeat tags$br 227 | #' 228 | #' @param times the number of br to return 229 | #' 230 | #' @return the number of br specified in times 231 | #' @noRd 232 | #' 233 | #' @examples 234 | #' rep_br(5) 235 | #' 236 | #' @importFrom shiny HTML 237 | rep_br <- function(times = 1) { 238 | HTML(rep("
", times = times)) 239 | } 240 | 241 | #' Create an url 242 | #' 243 | #' @param url the URL 244 | #' @param text the text to display 245 | #' 246 | #' @return an a tag 247 | #' @noRd 248 | #' 249 | #' @examples 250 | #' enurl("https://www.thinkr.fr", "ThinkR") 251 | #' 252 | #' @importFrom shiny tags 253 | enurl <- function(url, text){ 254 | tags$a(href = url, text) 255 | } 256 | 257 | #' Columns wrappers 258 | #' 259 | #' These are convenient wrappers around 260 | #' `column(12, ...)`, `column(6, ...)`, `column(4, ...)`... 261 | #' 262 | #' @noRd 263 | #' 264 | #' @importFrom shiny column 265 | col_12 <- function(...){ 266 | column(12, ...) 267 | } 268 | 269 | #' @importFrom shiny column 270 | col_10 <- function(...){ 271 | column(10, ...) 272 | } 273 | 274 | #' @importFrom shiny column 275 | col_8 <- function(...){ 276 | column(8, ...) 277 | } 278 | 279 | #' @importFrom shiny column 280 | col_6 <- function(...){ 281 | column(6, ...) 282 | } 283 | 284 | 285 | #' @importFrom shiny column 286 | col_4 <- function(...){ 287 | column(4, ...) 288 | } 289 | 290 | 291 | #' @importFrom shiny column 292 | col_3 <- function(...){ 293 | column(3, ...) 294 | } 295 | 296 | 297 | #' @importFrom shiny column 298 | col_2 <- function(...){ 299 | column(2, ...) 300 | } 301 | 302 | 303 | #' @importFrom shiny column 304 | col_1 <- function(...){ 305 | column(1, ...) 306 | } 307 | 308 | 309 | 310 | #' Make the current tag behave like an action button 311 | #' 312 | #' Only works with compatible tags like button or links 313 | #' 314 | #' @param tag Any compatible tag. 315 | #' @param inputId Unique id. This will host the input value to be used 316 | #' on the server side. 317 | #' 318 | #' @return The modified tag with an extra id and the action button class. 319 | #' @noRd 320 | #' 321 | #' @examples 322 | #' if (interactive()) { 323 | #' library(shiny) 324 | #' 325 | #' link <- a(href = "#", "My super link", style = "color: lightblue;") 326 | #' 327 | #' ui <- fluidPage( 328 | #' make_action_button(link, inputId = "mylink") 329 | #' ) 330 | #' 331 | #' server <- function(input, output, session) { 332 | #' observeEvent(input$mylink, { 333 | #' showNotification("Pouic!") 334 | #' }) 335 | #' } 336 | #' 337 | #' shinyApp(ui, server) 338 | #' 339 | #' } 340 | make_action_button <- function(tag, inputId = NULL) { 341 | # some obvious checks 342 | if (!inherits(tag, "shiny.tag")) stop("Must provide a shiny tag.") 343 | if (!is.null(tag$attribs$class)) { 344 | if (grep("action-button", tag$attribs$class)) { 345 | stop("tag is already an action button") 346 | } 347 | } 348 | if (is.null(inputId) && is.null(tag$attribs$id)) { 349 | stop("tag does not have any id. Please use inputId to be able to 350 | access it on the server side.") 351 | } 352 | 353 | # handle id 354 | if (!is.null(inputId)) { 355 | if (!is.null(tag$attribs$id)) { 356 | warning( 357 | paste( 358 | "tag already has an id. Please use input$", 359 | tag$attribs$id, 360 | "to access it from the server side. inputId will be ignored." 361 | ) 362 | ) 363 | } else { 364 | tag$attribs$id <- inputId 365 | } 366 | } 367 | 368 | # handle class 369 | if (is.null(tag$attribs$class)) { 370 | tag$attribs$class <- "action-button" 371 | } else { 372 | tag$attribs$class <- paste(tag$attribs$class, "action-button") 373 | } 374 | # return tag 375 | tag 376 | } 377 | 378 | 379 | # UNCOMMENT AND USE 380 | # 381 | # usethis::use_package("markdown") 382 | # usethis::use_package("rmarkdown") 383 | # 384 | # To use this part of the UI 385 | # 386 | #' #' Include Content From a File 387 | #' #' 388 | #' #' Load rendered RMarkdown from a file and turn into HTML. 389 | #' #' 390 | #' #' @rdname includeRMarkdown 391 | #' #' @export 392 | #' #' 393 | #' #' @importFrom rmarkdown render 394 | #' #' @importFrom markdown markdownToHTML 395 | #' #' @importFrom shiny HTML 396 | #' includeRMarkdown <- function(path){ 397 | #' 398 | #' md <- tempfile(fileext = '.md') 399 | #' 400 | #' on.exit(unlink(md),add = TRUE) 401 | #' 402 | #' rmarkdown::render( 403 | #' path, 404 | #' output_format = 'md_document', 405 | #' output_dir = tempdir(), 406 | #' output_file = md,quiet = TRUE 407 | #' ) 408 | #' 409 | #' html <- markdown::markdownToHTML(md, fragment.only = TRUE) 410 | #' 411 | #' Encoding(html) <- "UTF-8" 412 | #' 413 | #' return(HTML(html)) 414 | #' } 415 | -------------------------------------------------------------------------------- /R/mod_cal_viewer_fct_helpers.R: -------------------------------------------------------------------------------- 1 | #' @importFrom twitchr get_users 2 | #' @importFrom dplyr filter 3 | #' @noRd 4 | get_twitch_id <- function(user_name) { 5 | user <- get_users(login = user_name) 6 | message(glue::glue("user_name: {user_name} - id: {x}", x = user$id)) 7 | res <- dplyr::select(user, id, description, profile_image_url) 8 | return(res) 9 | } 10 | 11 | parse_duration <- function(x, 12 | time_unit = c("seconds", "minutes", "hours"), 13 | dur_regex = "([0-9]{1,2}h)?([0-9]{1,2}m)?([0-9]{1,2}(\\.[0-9]{1,3})?s)?") { 14 | # process to reverse engineer the starting time of each video 15 | # clever regex found at https://stackoverflow.com/a/11293491 16 | # we get a matrix back with 2 rows (second row is meaningless) 17 | # columns are the following 18 | # - col 1: the raw duration string 19 | # - col 2: the "hour" part of the duration string (ex "1h") 20 | # - col 3: the "minute" part of the duration string (ex "1m") 21 | # - col 4: the "second" part of the duration string (ex "1s") 22 | # - col 5: meaningless 23 | time_unit <- match.arg(time_unit) 24 | dur_parsed <- stringr::str_match_all(x, dur_regex)[[1]] 25 | 26 | # set up final duration 27 | dur_final <- 0 28 | 29 | # extract relevant parts of duration matrix 30 | dur_vec <- readr::parse_number(dur_parsed[1, c(2,3,4)]) 31 | 32 | names(dur_vec) <- c("hours", "minutes", "seconds") 33 | 34 | time_mult <- switch( 35 | time_unit, 36 | hours = c(1, (1/60), (1/3600)), 37 | minutes = c(60, 1, (1/60)), 38 | seconds = c(3600, 60, 1) 39 | ) 40 | 41 | dur_vec <- sum(dur_vec * time_mult) 42 | 43 | return(dur_vec) 44 | 45 | } 46 | 47 | # https://stackoverflow.com/questions/27397332/find-round-the-time-to-nearest-half-an-hour-from-a-single-file 48 | round_time <- function(x) { 49 | x <- as.POSIXlt(x) 50 | 51 | x$min <- round(x$min / 30) * 30 52 | x$sec <- floor(x$sec / 60) 53 | 54 | x <- as.POSIXct(x) 55 | return(x) 56 | } 57 | 58 | shift_week <- function(x, convert = TRUE, which = "next") { 59 | if (!convert) { 60 | return(x) 61 | } else { 62 | # get current day of week from supplied date 63 | x_weekday <- clock::as_year_month_weekday(x) %>% clock::get_day() 64 | 65 | res <- clock::date_shift(x, target = clock::weekday(x_weekday), which = which, boundary = "advance") 66 | 67 | return(res) 68 | } 69 | } 70 | 71 | compute_end_clock <- function(start_clock, stream_length, precision = "hour") { 72 | # truncate the stream length to floor of nearest hour 73 | new_length <- clock::duration_round(clock::duration_seconds(stream_length), precision = precision) 74 | end_clock <- clock::add_hours(start_clock, new_length) 75 | return(end_clock) 76 | } 77 | 78 | time_parser <- function(x, orig_zone = "UTC", new_zone = "America/New_York", format = "%Y-%m-%dT%H:%M:%SZ", ambiguous = "earliest", convert_to_char = TRUE) { 79 | # was format = "%Y%m%dT%H%M%S" for ical 80 | 81 | x <- clock::date_time_parse(x, orig_zone, format = format, ambiguous = ambiguous) 82 | x_z <- clock::as_zoned_time(x) 83 | 84 | # change to the desired time zone 85 | x_final <- clock::zoned_time_set_zone(x_z, new_zone) %>% clock::as_naive_time() 86 | 87 | if (convert_to_char) { 88 | x_final <- as.character(x_final) 89 | } 90 | return(x_final) 91 | } 92 | 93 | 94 | get_twitch_schedule <- function(id) { 95 | r <- httr::GET("https://api.twitch.tv/helix/schedule", query = list(broadcaster_id = id)) 96 | status <- httr::status_code(r) 97 | 98 | if (status != 200) { 99 | warning(glue::glue("User {id} does not have valid schedule data. Proceeding to infer a schedule based on videos uploaded (status code {status})")) 100 | r <- httr::GET("https://api.twitch.tv/helix/videos", query = list(user_id = id, period = "week")) 101 | status <- httr::status_code(r) 102 | 103 | if (status != 200) { 104 | warning(glue::glue("User {id} does not have any videos! Skipping ...")) 105 | return(NULL) 106 | } else { 107 | current_weekday <- clock::date_now("America/New_York") %>% 108 | clock::as_year_month_weekday() %>% 109 | clock::get_day() 110 | 111 | prev_week_date <- clock::date_now("America/New_York") %>% 112 | clock::date_shift(target = clock::weekday(current_weekday), which = "previous", boundary = "advance") 113 | 114 | current_sunday <- clock::date_now("America/New_York") %>% 115 | clock::date_shift(target = clock::weekday(clock::clock_weekdays$sunday), which = "previous") 116 | 117 | res <- httr::content(r, "parsed") %>% 118 | purrr::pluck("data") %>% 119 | tibble::tibble() %>% 120 | tidyr::unnest_wider(1) 121 | 122 | res_int <- res %>% 123 | mutate(start = purrr::map(created_at, ~time_parser(.x, convert_to_char = FALSE))) %>% 124 | mutate(start = purrr::map(start, ~clock::as_date_time(.x, zone = "America/New_York"))) %>% 125 | mutate(duration2 = purrr::map_dbl(duration, ~parse_duration(.x, "seconds"))) %>% 126 | tidyr::unnest(cols = c(start)) %>% 127 | mutate(start = purrr::map(start, ~round_time(.x))) %>% 128 | mutate(end = purrr::map2(start, duration2, ~compute_end_clock(.x, .y))) %>% 129 | mutate(category = "time", 130 | recurrenceRule = "Every week", 131 | start_time = NA, 132 | end_time = NA) %>% 133 | tidyr::unnest(cols = c(start, end)) %>% 134 | filter(start > prev_week_date) 135 | 136 | if (nrow(res_int) < 1) { 137 | return(NULL) 138 | } else { 139 | res_final <- res_int %>% 140 | mutate(before_week_ind = start < current_sunday) %>% 141 | mutate(start = purrr::map2(start, before_week_ind, ~shift_week(.x, .y))) %>% 142 | mutate(end = purrr::map2(end, before_week_ind, ~shift_week(.x, .y))) %>% 143 | tidyr::unnest(cols = c(start, end)) %>% 144 | mutate(start = as.character(start), end = as.character(end)) %>% 145 | dplyr::select(start_time, start, end_time, end, title, category, recurrenceRule) 146 | } 147 | } 148 | } else { 149 | res <- httr::content(r, "parsed") %>% 150 | purrr::pluck("data", "segments") %>% 151 | tibble::tibble() %>% 152 | tidyr::unnest_wider(1) 153 | 154 | res_int <- res %>% 155 | mutate(start = purrr::map(start_time, ~time_parser(.x, convert_to_char = FALSE)), 156 | end = purrr::map(end_time, ~time_parser(.x, convert_to_char = FALSE)), 157 | category = "time", 158 | recurrenceRule = "Every week") %>% 159 | dplyr::select(start_time, start, end_time, end, title, category, recurrenceRule) 160 | #tidyr::unnest(cols = c(start, end)) 161 | 162 | # grab the first records of each unique stream 163 | res_first <- res_int %>% 164 | dplyr::group_by(title) %>% 165 | dplyr::arrange(title, start) %>% 166 | dplyr::slice(1) %>% 167 | dplyr::ungroup() %>% 168 | mutate(start = purrr::map(start, ~clock::as_date_time(.x, zone = "America/New_York")), 169 | end = purrr::map(end, ~clock::as_date_time(.x, zone = "America/New_York"))) %>% 170 | mutate(start = purrr::map(start, ~shift_week(.x, which = "previous")), 171 | end = purrr::map(end, ~shift_week(.x, which = "previous"))) %>% 172 | mutate(start = purrr::map(start, ~clock::as_naive_time(.x)), 173 | end = purrr::map(end, ~clock::as_naive_time(.x))) 174 | 175 | # bind back together 176 | res_final <- dplyr::bind_rows( 177 | tidyr::unnest(res_first, c("start", "end")), 178 | tidyr::unnest(res_int, c("start", "end")) 179 | ) %>% 180 | mutate(start = as.character(start), end = as.character(end)) 181 | 182 | 183 | } 184 | return(res_final) 185 | } 186 | 187 | 188 | get_twitch_videos <- function(id) { 189 | message(glue::glue("twitch id {id}")) 190 | videos <- twitchr::get_videos(user_id = id, first = 100) 191 | 192 | if (is.null(videos)) { 193 | # try getting clips instead 194 | videos <- twitchr::get_all_clips(broadcaster_id = id) 195 | if (is.null(videos)) { 196 | warning(glue::glue("There are no videos for user {id}")) 197 | return(NA) 198 | } else { 199 | videos_play <- videos %>% 200 | dplyr::mutate(video_id = purrr::map_chr(url, ~{ 201 | tmp <- stringr::str_split(.x, "/") 202 | n_items <- length(tmp[[1]]) 203 | res <- tmp[[1]][n_items] 204 | return(res) 205 | })) %>% 206 | dplyr::slice(1) %>% 207 | dplyr::pull(video_id) 208 | return(videos_play) 209 | } 210 | } 211 | 212 | videos_play <- videos$data %>% 213 | #tibble() %>% 214 | dplyr::mutate(video_id = purrr::map_chr(url, ~{ 215 | tmp <- stringr::str_split(.x, "/") 216 | n_items <- length(tmp[[1]]) 217 | res <- tmp[[1]][n_items] 218 | return(res) 219 | })) %>% 220 | dplyr::slice(1) %>% 221 | dplyr::pull(video_id) 222 | 223 | return(videos_play) 224 | } 225 | 226 | 227 | #' Import calendar directly from server 228 | #' 229 | #' @param cal_slug string for URL slug of calendar. 230 | #' 231 | #' @return data frame with calendar event contents 232 | #' @export 233 | #' @import caldav 234 | #' @importFrom calendar ic_read ical 235 | import_cal <- function(cal_slug = "wimpys-world-of-streamers", cal_base_url = NULL) { 236 | if (is.null(cal_base_url)) { 237 | cal_base_url <- get_golem_config("cal_base_url") 238 | } 239 | 240 | caldav_url = glue::glue("{cal_base_url}/{cal_slug}") 241 | cal_data <- 242 | caldav::caldav_get_all_simple_auth( 243 | url = caldav_url, 244 | user = Sys.getenv("NEXTCLOUD_USER"), 245 | password = Sys.getenv("NEXTCLOUD_PASSWORD") 246 | ) 247 | 248 | x <- cal_data$calendar 249 | 250 | res <- withr::with_tempfile("tf", { 251 | cat(x, file = tf) 252 | ic_read(tf) 253 | }) 254 | 255 | return(res) 256 | } 257 | 258 | #' @importFrom dplyr mutate select left_join filter case_when 259 | #' @importFrom clock date_time_parse as_naive_time as_zoned_time zoned_time_set_zone 260 | #' @importFrom purrr map map2 261 | #' @noRd 262 | process_cal <- function(raw_df) { 263 | dt_df <- raw_df %>% 264 | mutate(uid = UID) %>% 265 | select(uid, starts_with("DTSTART"), starts_with("DTEND")) %>% 266 | tidyr::pivot_longer(!uid, names_to = c("dt_type", "timezone"), names_sep = ";", values_to = "time") %>% 267 | filter(!is.na(time)) %>% 268 | tidyr::pivot_wider(names_from = c(dt_type), values_from = time) 269 | 270 | dt_df2 <- dt_df %>% 271 | mutate( 272 | timezone = stringr::str_remove_all(timezone, "TZID="), 273 | start_clock = purrr::map2(DTSTART, timezone, ~time_parser(.x, .y)), 274 | #start = as.character(start_clock), 275 | end_clock = purrr::map2(DTEND, timezone, ~time_parser(.x, .y)), 276 | #end = as.character(end_clock) 277 | ) %>% 278 | select(uid, start_clock, end_clock, timezone) %>% 279 | tidyr::unnest(cols = c(start_clock, end_clock)) 280 | 281 | rec_df <- raw_df %>% 282 | mutate(uid = UID) %>% 283 | mutate(recurrenceRule = case_when( 284 | stringr::str_detect(RRULE, "FREQ=WEEKLY") ~ "Every week", 285 | TRUE ~ RRULE 286 | )) %>% 287 | select(uid, recurrenceRule) 288 | 289 | final_df <- raw_df %>% 290 | mutate(uid = UID) %>% 291 | select(uid, title = SUMMARY, location = LOCATION) %>% 292 | left_join(dt_df2, by = "uid") %>% 293 | left_join(rec_df, by = "uid") %>% 294 | mutate(raw = uid) 295 | 296 | return(final_df) 297 | } 298 | 299 | process_raw_timezones <- function() { 300 | source_html <- "inst/app/www/timezones_raw.html" 301 | 302 | # ALL credit goes to Tan for rescuing me yet again! 303 | y <- rvest::read_html(source_html) %>% 304 | rvest::html_element("form select") %>% 305 | rvest::html_children() 306 | 307 | timezone_res <- tibble::tibble( 308 | label = y %>% rvest::html_attr("label"), 309 | value = y %>% rvest::html_attr("value") 310 | ) %>% 311 | tidyr::fill(label) %>% 312 | dplyr::filter(!is.na(value)) %>% 313 | dplyr::group_by(label) %>% 314 | dplyr::summarise(value = list(value)) %>% 315 | tibble::deframe() 316 | 317 | return(timezone_res) 318 | } -------------------------------------------------------------------------------- /R/mod_cal_viewer.R: -------------------------------------------------------------------------------- 1 | #' cal_viewer UI Function 2 | #' 3 | #' @description A shiny Module. 4 | #' 5 | #' @param id,input,output,session Internal parameters for {shiny}. 6 | #' 7 | #' @noRd 8 | #' 9 | #' @importFrom shiny NS tagList 10 | #' @import toastui 11 | #' @import shinylogs 12 | mod_cal_viewer_ui <- function(id){ 13 | ns <- NS(id) 14 | tagList( 15 | fluidRow( 16 | col_3( 17 | radioButtons( 18 | ns("cal_view"), 19 | label = "View Type", 20 | choices = c("day", "week", "month"), 21 | selected = "week", 22 | inline = TRUE 23 | ) 24 | ), 25 | col_3( 26 | selectInput( 27 | ns("streamer_select"), 28 | label = "Streamer", 29 | choices = c("nothing") 30 | ) 31 | ), 32 | col_3( 33 | colourpicker::colourInput( 34 | ns("entry_color"), 35 | label = "Entry Color", 36 | value = "#0ff1a6" 37 | ) 38 | ), 39 | col_3( 40 | colourpicker::colourInput( 41 | ns("entry_font_color"), 42 | label = "Entry Font Color", 43 | palette = "limited", 44 | value = "black", 45 | allowedCols = c("white", "black") 46 | ) 47 | ) 48 | ), 49 | fluidRow( 50 | col_4( 51 | shinyWidgets::pickerInput( 52 | ns("time_zone"), 53 | label = "Select Time Zone", 54 | choices = c("nothing"), 55 | options = shinyWidgets::pickerOptions(liveSearch = TRUE) 56 | ) 57 | ) 58 | ), 59 | fluidRow( 60 | col_8( 61 | calendarOutput(ns("calui")) 62 | ), 63 | col_4( 64 | h4("Click an entry to view the streamer's latest VOD!"), 65 | uiOutput(ns("vid")) 66 | ) 67 | ) 68 | ) 69 | } 70 | 71 | #' cal_viewer Server Functions 72 | #' 73 | #' @noRd 74 | mod_cal_viewer_server <- function(id){ 75 | moduleServer( id, function(input, output, session){ 76 | ns <- session$ns 77 | 78 | # load data set from online storage if running app on production 79 | if (getOption("golem.app.prod")) { 80 | cal_df <- readRDS(url("https://sds-streamer-data.us-east-1.linodeobjects.com/streamer_data_current.rds", "rb")) 81 | } else { 82 | cal_df <- readRDS("prototyping/streamer_data_current.rds") 83 | } 84 | 85 | # perform schedule munging that does not depend on UI 86 | cal_sub <- cal_df %>% 87 | tidyr::unnest(schedule_data) %>% 88 | mutate(calendarId = 1) %>% 89 | mutate(id = seq_len(dplyr::n())) %>% 90 | mutate(raw = map2(title, categories, ~list(title = .x, categories = .y))) 91 | 92 | # reactive value for clicked schedule item 93 | schedule_click <- reactiveVal(NULL) 94 | 95 | # reactive for source schedule data 96 | cal_display_df <- reactiveVal(NULL) 97 | 98 | # reactive for time zone selected 99 | cal_prev_timezone <- reactiveVal(NULL) 100 | 101 | observeEvent(input$fancy, { 102 | schedule_click(input$fancy) 103 | }) 104 | 105 | output$vid <- renderUI({ 106 | #req(cal_processed()) 107 | req(schedule_click()) 108 | 109 | video_id <- cal_sub %>% 110 | dplyr::filter(id == schedule_click()$id) %>% 111 | tidyr::unnest(cols = video_data) %>% 112 | dplyr::pull(video_data) 113 | 114 | js_snippet <- glue::glue(" 115 | ", 126 | divid = ns("testvid"), 127 | .open = "<<<", 128 | .close = ">>>") 129 | 130 | tags$div( 131 | id = ns("testvid"), 132 | #h2("vid was here") 133 | htmltools::HTML(js_snippet) 134 | ) 135 | }) 136 | 137 | output$last_changed <- renderPrint({ 138 | 139 | req(input$calui_schedules) 140 | 141 | input_lst <- reactiveValuesToList(input) 142 | print(input_lst) 143 | # list( 144 | # click = input$calui_click, 145 | # dates = input$calui_dates, 146 | # schedules = input$calui_schedules 147 | # ) 148 | #input$`.shinylogs_lastInput` 149 | # Shiny.setInputValue(el.id + "_dates" 150 | # _add 151 | # _schedules 152 | # _click 153 | # _delete 154 | # _update 155 | # _dates 156 | }) 157 | 158 | # on initial load, ensure calendar data matches user's time zone 159 | observeEvent(session$userData$zone, { 160 | message("entered zone observe") 161 | 162 | 163 | # update choices for streamer selection input 164 | shiny::updateSelectInput( 165 | session = session, 166 | inputId = "streamer_select", 167 | choices = c("all", cal_df$user_id), 168 | selected = "all" 169 | ) 170 | 171 | # set default in picker input to user's time zone 172 | shinyWidgets::updatePickerInput( 173 | session = session, 174 | inputId = "time_zone", 175 | choices = process_raw_timezones(), 176 | selected = session$userData$zone() 177 | ) 178 | 179 | # apply client time zone to schedule data 180 | new_zone <- session$userData$zone() 181 | 182 | cal_sub2 <- cal_sub %>% 183 | mutate(start = purrr::map_chr(start, ~time_parser(.x, orig_zone = "America/New_York", new_zone = new_zone, format = "%Y-%m-%dT%H:%M:%S", convert_to_char = TRUE))) %>% 184 | mutate(end = purrr::map_chr(end, ~time_parser(.x, orig_zone = "America/New_York", new_zone = new_zone, format = "%Y-%m-%dT%H:%M:%S", convert_to_char = TRUE))) %>% 185 | select(., -video_data, -start_time, -end_time, -category) 186 | 187 | cal_display_df(cal_sub2) 188 | }) 189 | 190 | observeEvent(input$time_zone, { 191 | req(session$userData$zone()) 192 | req(cal_display_df()) 193 | 194 | # on default render (when no choices have been updated), do nothing 195 | if (input$time_zone == "nothing") return(NULL) 196 | 197 | # update schedule data with new time zone 198 | prev_zone <- session$userData$zone() 199 | 200 | if (!is.null(cal_prev_timezone())) { 201 | prev_zone <- cal_prev_timezone() 202 | } 203 | 204 | # apply selected time zone to schedule data 205 | if (input$time_zone != prev_zone) { 206 | 207 | new_zone <- input$time_zone 208 | cal_prev_timezone(new_zone) 209 | 210 | cal_sub2 <- cal_display_df() %>% 211 | mutate(start = purrr::map_chr(start, ~time_parser(.x, orig_zone = prev_zone, new_zone = new_zone, format = "%Y-%m-%d %H:%M:%S", convert_to_char = TRUE))) %>% 212 | mutate(end = purrr::map_chr(end, ~time_parser(.x, orig_zone = prev_zone, new_zone = new_zone, format = "%Y-%m-%d %H:%M:%S", convert_to_char = TRUE))) 213 | 214 | cal_display_df(cal_sub2) 215 | } 216 | }) 217 | 218 | # reactive for calendar object 219 | cal_display_obj <- reactive({ 220 | req(cal_display_df()) 221 | req(input$entry_color) 222 | req(input$entry_font_color) 223 | 224 | # apply background and font colors to calendar entries 225 | cal_tmp <- cal_display_df() %>% 226 | mutate(bgColor = ifelse(is.na(bgColor), input$entry_color, bgColor)) %>% 227 | mutate(color = ifelse(is.na(color), input$entry_font_color, color)) 228 | 229 | # filter for selected streamers if not "all" 230 | if (input$streamer_select != "nothing") { 231 | if (input$streamer_select != "all") { 232 | cal_tmp <- cal_display_df() %>% 233 | filter(user_id == input$streamer_select) 234 | } 235 | } 236 | 237 | cal_zone <- session$userData$zone() 238 | offsetCalculator <- NULL 239 | 240 | #---------------------------------------------------------------------------- 241 | # I tried this to get the current time shown on the calendar to match 242 | # the selected time zone, but I am clearly doing it wrong 243 | #---------------------------------------------------------------------------- 244 | # if (input$time_zone == "nothing") { 245 | # cal_zone <- session$userData$zone() 246 | # offsetCalculator <- NULL 247 | # } else { 248 | # cal_zone <- input$time_zone 249 | # offsetCalculator <- 'function(timezoneName, timestamp){ return moment.tz.zone(timezoneName).utcOffset(timestamp); }' 250 | # } 251 | 252 | my_id <- ns("fancy") 253 | toastui::calendar( 254 | #toastui::cal_demo_data(), 255 | cal_tmp, 256 | view = input$cal_view, 257 | scheduleView = list('time'), 258 | useNavigation = TRUE, 259 | useDetailPopup = FALSE, 260 | ) %>% 261 | cal_events( 262 | # schedule elements that we could grab 263 | # - location 264 | # - recurrenceRule 265 | # - body 266 | # - title 267 | cal = ., 268 | clickSchedule = JS(glue::glue('function(event) {console.log(event.schedule.id); Shiny.setInputValue("<>", {raw: event.schedule.raw, id: event.schedule.id, x: event.event.clientX, y: event.event.clientY});}', .open = "<<", .close = ">>")) 269 | ) %>% 270 | cal_timezone( 271 | timezoneName = cal_zone, 272 | displayLabel = NULL, 273 | offsetCalculator = offsetCalculator 274 | ) %>% 275 | cal_week_options( 276 | showTimezoneCollapseButton = TRUE, 277 | timezonesCollapsed = FALSE 278 | ) 279 | # cal_theme( 280 | # common.border = "2px solid #E5E9F0", 281 | # month.dayname.borderLeft = "2px solid #E5E9F0", 282 | # common.backgroundColor = "#2E3440", 283 | # #month.dayExceptThisMonth.color = input$entry_color, 284 | # #common.backgroundColor = input$entry_color, 285 | # common.holiday.color = "#88C0D0", 286 | # common.saturday.color = "#88C0D0", 287 | # common.dayname.color = "#ECEFF4", 288 | # common.today.color = "#333" 289 | # ) 290 | }) 291 | 292 | output$calui <- renderCalendar({ 293 | req(cal_display_obj()) 294 | cal_display_obj() 295 | }) 296 | 297 | # update calendar cell colors when toggled 298 | observeEvent(input$entry_color, { 299 | # TODO: Keep this in case I need it later 300 | #message(glue::glue("color is {color}", color = input$entry_color)) 301 | }) 302 | 303 | observeEvent(input$fancy, { 304 | removeUI(selector = paste0("#", ns("custom_popup"))) 305 | id <- as.numeric(input$fancy$id) 306 | # Get the appropriate line clicked 307 | sched <- cal_sub[cal_sub$id == id, ] 308 | 309 | start_time <- lubridate::as_datetime(sched$start) %>% hms::as_hms() 310 | end_time <- lubridate::as_datetime(sched$end) %>% hms::as_hms() 311 | 312 | insertUI( 313 | selector = "body", 314 | #selector = paste0("#", ns("add")), 315 | ui = absolutePanel( 316 | id = ns("custom_popup"), 317 | top = input$fancy$y, 318 | left = input$fancy$x, 319 | draggable = FALSE, 320 | width = "300px", 321 | tags$div( 322 | style = "background: #FFF; padding: 10px; box-shadow: 0px 0.2em 0.4em rgb(0, 0, 0, 0.8); border-radius: 5px;", 323 | actionLink( 324 | inputId = ns("close_calendar_panel"), 325 | label = NULL, 326 | icon = icon("close"), 327 | style = "position: absolute; top: 5px; right: 5px;" 328 | ), 329 | tags$br(), 330 | tags$div( 331 | style = "text-align: left;", 332 | tags$p( 333 | glue::glue("{user_id} is streaming from {start_time} to {end_time}", 334 | user_id = sched$user_id) 335 | ), 336 | tags$p( 337 | sched$description, 338 | ), 339 | tags$p( 340 | "Categories", list_to_li(sched$raw[[1]]$categories) 341 | ) 342 | ) 343 | ) 344 | ) 345 | ) 346 | }) 347 | 348 | observeEvent(input$close_calendar_panel, { 349 | removeUI(selector = paste0("#", ns("custom_popup"))) 350 | }) 351 | }) 352 | } 353 | 354 | ## To be copied in the UI 355 | # mod_cal_viewer_ui("cal_viewer_ui_1") 356 | 357 | ## To be copied in the server 358 | # mod_cal_viewer_server("cal_viewer_ui_1") 359 | -------------------------------------------------------------------------------- /dev/r_code/tmp.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | PRODID:-//IDN nextcloud.com//Calendar app 2.2.0//EN 3 | CALSCALE:GREGORIAN 4 | VERSION:2.0 5 | BEGIN:VEVENT 6 | CREATED:20210512T060448Z 7 | DTSTAMP:20210512T060706Z 8 | LAST-MODIFIED:20210512T060706Z 9 | SEQUENCE:2 10 | UID:2f4bbb1a-5f0f-43d3-a3c4-b01a8d889a84 11 | DTSTART;TZID=Europe/Zurich:20210514T210000 12 | DTEND;TZID=Europe/Zurich:20210514T220000 13 | SUMMARY:StreamBerry : a DIY StreamDeck\, part 1 14 | LOCATION:Twitch 15 | DESCRIPTION:Let's do some Python magic\, and make a client program connect to a server program with zero configuration. 16 | END:VEVENT 17 | BEGIN:VTIMEZONE 18 | TZID:Europe/Zurich 19 | BEGIN:DAYLIGHT 20 | TZOFFSETFROM:+0100 21 | TZOFFSETTO:+0200 22 | TZNAME:CEST 23 | DTSTART:19700329T020000 24 | RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU 25 | END:DAYLIGHT 26 | BEGIN:STANDARD 27 | TZOFFSETFROM:+0200 28 | TZOFFSETTO:+0100 29 | TZNAME:CET 30 | DTSTART:19701025T030000 31 | RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU 32 | END:STANDARD 33 | END:VTIMEZONE 34 | END:VCALENDAR BEGIN:VCALENDAR 35 | PRODID:-//IDN nextcloud.com//Calendar app 2.2.0//EN 36 | CALSCALE:GREGORIAN 37 | VERSION:2.0 38 | BEGIN:VEVENT 39 | CREATED:20210524T203932Z 40 | DTSTAMP:20210524T204054Z 41 | LAST-MODIFIED:20210524T204054Z 42 | SEQUENCE:3 43 | UID:397edb19-e6f6-46cf-9640-5b8932729fad 44 | DTSTART;TZID=Europe/Ljubljana:20210529T223000 45 | DTEND;TZID=Europe/Ljubljana:20210530T003000 46 | SUMMARY:Bigpod's Tries to implement all he learned about python in actual program 47 | END:VEVENT 48 | BEGIN:VTIMEZONE 49 | TZID:Europe/Ljubljana 50 | BEGIN:DAYLIGHT 51 | TZOFFSETFROM:+0100 52 | TZOFFSETTO:+0200 53 | TZNAME:CEST 54 | DTSTART:19700329T020000 55 | RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU 56 | END:DAYLIGHT 57 | BEGIN:STANDARD 58 | TZOFFSETFROM:+0200 59 | TZOFFSETTO:+0100 60 | TZNAME:CET 61 | DTSTART:19701025T030000 62 | RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU 63 | END:STANDARD 64 | END:VTIMEZONE 65 | END:VCALENDAR BEGIN:VCALENDAR 66 | PRODID:-//IDN nextcloud.com//Calendar app 2.2.0//EN 67 | CALSCALE:GREGORIAN 68 | VERSION:2.0 69 | BEGIN:VEVENT 70 | CREATED:20210524T204217Z 71 | DTSTAMP:20210524T204305Z 72 | LAST-MODIFIED:20210524T204305Z 73 | SEQUENCE:2 74 | UID:50e7645d-2165-4434-bce0-7b38e64c8bbf 75 | DTSTART;TZID=Europe/Ljubljana:20210602T223000 76 | DTEND;TZID=Europe/Ljubljana:20210603T003000 77 | SUMMARY:Bigpod's stream about repositoryManagerNet 78 | END:VEVENT 79 | BEGIN:VTIMEZONE 80 | TZID:Europe/Ljubljana 81 | BEGIN:DAYLIGHT 82 | TZOFFSETFROM:+0100 83 | TZOFFSETTO:+0200 84 | TZNAME:CEST 85 | DTSTART:19700329T020000 86 | RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU 87 | END:DAYLIGHT 88 | BEGIN:STANDARD 89 | TZOFFSETFROM:+0200 90 | TZOFFSETTO:+0100 91 | TZNAME:CET 92 | DTSTART:19701025T030000 93 | RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU 94 | END:STANDARD 95 | END:VTIMEZONE 96 | END:VCALENDAR BEGIN:VCALENDAR 97 | PRODID:-//IDN nextcloud.com//Calendar app 2.2.0//EN 98 | CALSCALE:GREGORIAN 99 | VERSION:2.0 100 | BEGIN:VEVENT 101 | CREATED:20210529T161306Z 102 | DTSTAMP:20210529T161432Z 103 | LAST-MODIFIED:20210529T161432Z 104 | SEQUENCE:2 105 | UID:9824ae8c-357d-4a33-8ef8-2023621e972e 106 | DTSTART;TZID=Europe/Ljubljana:20210609T223000 107 | DTEND;TZID=Europe/Ljubljana:20210610T003000 108 | RRULE:FREQ=WEEKLY;BYDAY=WE;UNTIL=20210824T220000Z 109 | SUMMARY:bigpods stream titles TBD 110 | END:VEVENT 111 | BEGIN:VTIMEZONE 112 | TZID:Europe/Ljubljana 113 | BEGIN:DAYLIGHT 114 | TZOFFSETFROM:+0100 115 | TZOFFSETTO:+0200 116 | TZNAME:CEST 117 | DTSTART:19700329T020000 118 | RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU 119 | END:DAYLIGHT 120 | BEGIN:STANDARD 121 | TZOFFSETFROM:+0200 122 | TZOFFSETTO:+0100 123 | TZNAME:CET 124 | DTSTART:19701025T030000 125 | RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU 126 | END:STANDARD 127 | END:VTIMEZONE 128 | END:VCALENDAR BEGIN:VCALENDAR 129 | PRODID:-//IDN nextcloud.com//Calendar app 2.2.0//EN 130 | CALSCALE:GREGORIAN 131 | VERSION:2.0 132 | BEGIN:VEVENT 133 | CREATED:20210524T195012Z 134 | DTSTAMP:20210524T195236Z 135 | LAST-MODIFIED:20210524T195236Z 136 | SEQUENCE:3 137 | UID:f27c3ef5-8f33-4893-b612-22e6c5a32e21 138 | DTSTART;TZID=Europe/Zurich:20210526T210000 139 | DTEND;TZID=Europe/Zurich:20210526T230000 140 | SUMMARY:Tea\, Earl Grey\, Hot ! - Live recording 141 | LOCATION:YouTube + Twitch 142 | DESCRIPTION:We're going to Twitch for the 1st time ! 143 | END:VEVENT 144 | BEGIN:VTIMEZONE 145 | TZID:Europe/Zurich 146 | BEGIN:DAYLIGHT 147 | TZOFFSETFROM:+0100 148 | TZOFFSETTO:+0200 149 | TZNAME:CEST 150 | DTSTART:19700329T020000 151 | RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU 152 | END:DAYLIGHT 153 | BEGIN:STANDARD 154 | TZOFFSETFROM:+0200 155 | TZOFFSETTO:+0100 156 | TZNAME:CET 157 | DTSTART:19701025T030000 158 | RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU 159 | END:STANDARD 160 | END:VTIMEZONE 161 | END:VCALENDAR BEGIN:VCALENDAR 162 | PRODID:-//IDN nextcloud.com//Calendar app 2.2.0//EN 163 | CALSCALE:GREGORIAN 164 | VERSION:2.0 165 | BEGIN:VEVENT 166 | CREATED:20210510T195914Z 167 | DTSTAMP:20210510T200001Z 168 | LAST-MODIFIED:20210510T200001Z 169 | SEQUENCE:3 170 | UID:9a6c1e51-d481-4293-b981-32b4f005b437 171 | DTSTART;TZID=America/New_York:20210510T163000 172 | DTEND;TZID=America/New_York:20210510T173000 173 | SUMMARY:Monica - Learning Factory Pt. 2 (Twitch) 174 | END:VEVENT 175 | BEGIN:VTIMEZONE 176 | TZID:America/New_York 177 | BEGIN:DAYLIGHT 178 | TZOFFSETFROM:-0500 179 | TZOFFSETTO:-0400 180 | TZNAME:EDT 181 | DTSTART:19700308T020000 182 | RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU 183 | END:DAYLIGHT 184 | BEGIN:STANDARD 185 | TZOFFSETFROM:-0400 186 | TZOFFSETTO:-0500 187 | TZNAME:EST 188 | DTSTART:19701101T020000 189 | RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU 190 | END:STANDARD 191 | END:VTIMEZONE 192 | END:VCALENDAR BEGIN:VCALENDAR 193 | PRODID:-//IDN nextcloud.com//Calendar app 2.2.0//EN 194 | CALSCALE:GREGORIAN 195 | VERSION:2.0 196 | BEGIN:VEVENT 197 | CREATED:20210520T195427Z 198 | DTSTAMP:20210520T195506Z 199 | LAST-MODIFIED:20210520T195506Z 200 | SEQUENCE:2 201 | UID:6d7008da-5969-4f55-b0e3-c7e1861401c0 202 | DTSTART;TZID=Europe/Ljubljana:20210522T223000 203 | DTEND;TZID=Europe/Ljubljana:20210523T003000 204 | SUMMARY:bigpod trying to learn Python on stream 205 | END:VEVENT 206 | BEGIN:VTIMEZONE 207 | TZID:Europe/Ljubljana 208 | BEGIN:DAYLIGHT 209 | TZOFFSETFROM:+0100 210 | TZOFFSETTO:+0200 211 | TZNAME:CEST 212 | DTSTART:19700329T020000 213 | RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU 214 | END:DAYLIGHT 215 | BEGIN:STANDARD 216 | TZOFFSETFROM:+0200 217 | TZOFFSETTO:+0100 218 | TZNAME:CET 219 | DTSTART:19701025T030000 220 | RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU 221 | END:STANDARD 222 | END:VTIMEZONE 223 | END:VCALENDAR BEGIN:VCALENDAR 224 | PRODID:-//IDN nextcloud.com//Calendar app 2.2.0//EN 225 | CALSCALE:GREGORIAN 226 | VERSION:2.0 227 | BEGIN:VEVENT 228 | CREATED:20210524T195434Z 229 | DTSTAMP:20210524T195517Z 230 | LAST-MODIFIED:20210524T195517Z 231 | SEQUENCE:4 232 | UID:0172ff7b-44de-4836-b14b-7cebcc6f1835 233 | DTSTART;TZID=Europe/Zurich:20210527T220000 234 | DTEND;TZID=Europe/Zurich:20210527T230000 235 | SUMMARY:Software Boutique 2.0\, part 6 236 | LOCATION:YouTube 237 | END:VEVENT 238 | BEGIN:VTIMEZONE 239 | TZID:Europe/Zurich 240 | BEGIN:DAYLIGHT 241 | TZOFFSETFROM:+0100 242 | TZOFFSETTO:+0200 243 | TZNAME:CEST 244 | DTSTART:19700329T020000 245 | RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU 246 | END:DAYLIGHT 247 | BEGIN:STANDARD 248 | TZOFFSETFROM:+0200 249 | TZOFFSETTO:+0100 250 | TZNAME:CET 251 | DTSTART:19701025T030000 252 | RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU 253 | END:STANDARD 254 | END:VTIMEZONE 255 | END:VCALENDAR BEGIN:VCALENDAR 256 | CALSCALE:GREGORIAN 257 | VERSION:2.0 258 | PRODID:-//IDN nextcloud.com//Calendar app 2.2.0//EN 259 | BEGIN:VEVENT 260 | CREATED:20210528T131756Z 261 | DTSTAMP:20210528T132939Z 262 | LAST-MODIFIED:20210528T132939Z 263 | SEQUENCE:6 264 | UID:7726ab4c-0f9c-4fde-839a-076830ea1435 265 | DTSTART;TZID=Europe/Zurich:20210603T220000 266 | DTEND;TZID=Europe/Zurich:20210603T230000 267 | SUMMARY:Software Boutique 2.0 268 | RRULE:FREQ=WEEKLY;BYDAY=TH 269 | LOCATION:YouTube 270 | EXDATE;TZID=Europe/Zurich:20210603T220000 271 | END:VEVENT 272 | BEGIN:VTIMEZONE 273 | TZID:Europe/Zurich 274 | BEGIN:DAYLIGHT 275 | TZOFFSETFROM:+0100 276 | TZOFFSETTO:+0200 277 | TZNAME:CEST 278 | DTSTART:19700329T020000 279 | RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU 280 | END:DAYLIGHT 281 | BEGIN:STANDARD 282 | TZOFFSETFROM:+0200 283 | TZOFFSETTO:+0100 284 | TZNAME:CET 285 | DTSTART:19701025T030000 286 | RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU 287 | END:STANDARD 288 | END:VTIMEZONE 289 | END:VCALENDAR BEGIN:VCALENDAR 290 | PRODID:-//IDN nextcloud.com//Calendar app 2.2.0//EN 291 | CALSCALE:GREGORIAN 292 | VERSION:2.0 293 | BEGIN:VEVENT 294 | CREATED:20210524T132807Z 295 | DTSTAMP:20210524T132907Z 296 | LAST-MODIFIED:20210524T132907Z 297 | SEQUENCE:2 298 | UID:352ea3ae-982c-4b92-9271-3e5fdbfc85f6 299 | DTSTART;TZID=America/Indiana/Indianapolis:20210525T153000 300 | DTEND;TZID=America/Indiana/Indianapolis:20210525T163000 301 | SUMMARY:Using Docker Slim with R & Shiny containers (Eric and Martin) 302 | END:VEVENT 303 | BEGIN:VTIMEZONE 304 | TZID:America/Indiana/Indianapolis 305 | BEGIN:DAYLIGHT 306 | TZOFFSETFROM:-0500 307 | TZOFFSETTO:-0400 308 | TZNAME:EDT 309 | DTSTART:19700308T020000 310 | RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU 311 | END:DAYLIGHT 312 | BEGIN:STANDARD 313 | TZOFFSETFROM:-0400 314 | TZOFFSETTO:-0500 315 | TZNAME:EST 316 | DTSTART:19701101T020000 317 | RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU 318 | END:STANDARD 319 | END:VTIMEZONE 320 | END:VCALENDAR BEGIN:VCALENDAR 321 | CALSCALE:GREGORIAN 322 | VERSION:2.0 323 | PRODID:-//IDN nextcloud.com//Calendar app 2.2.0//EN 324 | BEGIN:VEVENT 325 | CREATED:20210424T210448Z 326 | DTSTAMP:20210428T200727Z 327 | LAST-MODIFIED:20210428T200727Z 328 | SEQUENCE:5 329 | UID:b146c8b0-6c21-4d0c-b00a-f018531e7c16 330 | DTSTART;TZID=Europe/Ljubljana:20210428T220000 331 | DTEND;TZID=Europe/Ljubljana:20210429T003000 332 | SUMMARY:Bigpod's stream about RepositoryManagerNet 333 | LOCATION:https://www.youtube.com/watch?v=aeDbsznAvKQ 334 | DESCRIPTION:https://www.youtube.com/watch?v=aeDbsznAvKQ 335 | END:VEVENT 336 | BEGIN:VTIMEZONE 337 | TZID:Europe/Ljubljana 338 | BEGIN:DAYLIGHT 339 | TZOFFSETFROM:+0100 340 | TZOFFSETTO:+0200 341 | TZNAME:CEST 342 | DTSTART:19700329T020000 343 | RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU 344 | END:DAYLIGHT 345 | BEGIN:STANDARD 346 | TZOFFSETFROM:+0200 347 | TZOFFSETTO:+0100 348 | TZNAME:CET 349 | DTSTART:19701025T030000 350 | RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU 351 | END:STANDARD 352 | END:VTIMEZONE 353 | END:VCALENDAR BEGIN:VCALENDAR 354 | CALSCALE:GREGORIAN 355 | VERSION:2.0 356 | PRODID:-//IDN nextcloud.com//Calendar app 2.2.0//EN 357 | BEGIN:VEVENT 358 | CREATED:20210524T194904Z 359 | DTSTAMP:20210524T195254Z 360 | LAST-MODIFIED:20210524T195254Z 361 | SEQUENCE:3 362 | UID:4206ab89-925a-4a99-8868-56ff280ef96e 363 | DTSTART;TZID=Europe/Zurich:20210525T223000 364 | DTEND;TZID=Europe/Zurich:20210525T233000 365 | SUMMARY:How to use Glimpse to automate the creation of stream titles 366 | LOCATION:Twitch 367 | END:VEVENT 368 | BEGIN:VTIMEZONE 369 | TZID:Europe/Zurich 370 | BEGIN:DAYLIGHT 371 | TZOFFSETFROM:+0100 372 | TZOFFSETTO:+0200 373 | TZNAME:CEST 374 | DTSTART:19700329T020000 375 | RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU 376 | END:DAYLIGHT 377 | BEGIN:STANDARD 378 | TZOFFSETFROM:+0200 379 | TZOFFSETTO:+0100 380 | TZNAME:CET 381 | DTSTART:19701025T030000 382 | RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU 383 | END:STANDARD 384 | END:VTIMEZONE 385 | END:VCALENDAR BEGIN:VCALENDAR 386 | PRODID:-//IDN nextcloud.com//Calendar app 2.2.0//EN 387 | CALSCALE:GREGORIAN 388 | VERSION:2.0 389 | BEGIN:VEVENT 390 | CREATED:20210423T201110Z 391 | DTSTAMP:20210423T201150Z 392 | LAST-MODIFIED:20210423T201150Z 393 | SEQUENCE:2 394 | UID:0a381960-39d1-4693-9e73-0624a53b8b1b 395 | DTSTART;TZID=Europe/Zurich:20210506T213000 396 | DTEND;TZID=Europe/Zurich:20210506T223000 397 | SUMMARY:Software Boutique 2.0\, part 3 398 | END:VEVENT 399 | BEGIN:VTIMEZONE 400 | TZID:Europe/Zurich 401 | BEGIN:DAYLIGHT 402 | TZOFFSETFROM:+0100 403 | TZOFFSETTO:+0200 404 | TZNAME:CEST 405 | DTSTART:19700329T020000 406 | RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU 407 | END:DAYLIGHT 408 | BEGIN:STANDARD 409 | TZOFFSETFROM:+0200 410 | TZOFFSETTO:+0100 411 | TZNAME:CET 412 | DTSTART:19701025T030000 413 | RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU 414 | END:STANDARD 415 | END:VTIMEZONE 416 | END:VCALENDAR BEGIN:VCALENDAR 417 | PRODID:-//IDN nextcloud.com//Calendar app 2.2.0//EN 418 | CALSCALE:GREGORIAN 419 | VERSION:2.0 420 | BEGIN:VEVENT 421 | CREATED:20210423T224127Z 422 | DTSTAMP:20210423T224431Z 423 | LAST-MODIFIED:20210423T224431Z 424 | SEQUENCE:3 425 | UID:c0e5837f-28b6-4a24-ad94-bb684d0d857a 426 | DTSTART;TZID=America/New_York:20210426T163000 427 | DTEND;TZID=America/New_York:20210426T180000 428 | SUMMARY:Monica's Oh My Git Part 2 livestream 429 | LOCATION:https://www.twitch.tv/communiteatime 430 | DESCRIPTION:More shenanigans with learning Git through games! 431 | END:VEVENT 432 | BEGIN:VTIMEZONE 433 | TZID:America/New_York 434 | BEGIN:DAYLIGHT 435 | TZOFFSETFROM:-0500 436 | TZOFFSETTO:-0400 437 | TZNAME:EDT 438 | DTSTART:19700308T020000 439 | RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU 440 | END:DAYLIGHT 441 | BEGIN:STANDARD 442 | TZOFFSETFROM:-0400 443 | TZOFFSETTO:-0500 444 | TZNAME:EST 445 | DTSTART:19701101T020000 446 | RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU 447 | END:STANDARD 448 | END:VTIMEZONE 449 | END:VCALENDAR BEGIN:VCALENDAR 450 | PRODID:-//IDN nextcloud.com//Calendar app 2.2.0//EN 451 | CALSCALE:GREGORIAN 452 | VERSION:2.0 453 | BEGIN:VEVENT 454 | CREATED:20210524T195527Z 455 | DTSTAMP:20210524T195620Z 456 | LAST-MODIFIED:20210524T195620Z 457 | SEQUENCE:2 458 | UID:a72d3df6-be41-4c26-8eb6-12e389f9b6ad 459 | DTSTART;TZID=Europe/Zurich:20210529T210000 460 | DTEND;TZID=Europe/Zurich:20210529T220000 461 | SUMMARY:StreamBerry\, part 2 462 | END:VEVENT 463 | BEGIN:VTIMEZONE 464 | TZID:Europe/Zurich 465 | BEGIN:DAYLIGHT 466 | TZOFFSETFROM:+0100 467 | TZOFFSETTO:+0200 468 | TZNAME:CEST 469 | DTSTART:19700329T020000 470 | RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU 471 | END:DAYLIGHT 472 | BEGIN:STANDARD 473 | TZOFFSETFROM:+0200 474 | TZOFFSETTO:+0100 475 | TZNAME:CET 476 | DTSTART:19701025T030000 477 | RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU 478 | END:STANDARD 479 | END:VTIMEZONE 480 | END:VCALENDAR BEGIN:VCALENDAR 481 | PRODID:-//IDN nextcloud.com//Calendar app 2.2.0//EN 482 | CALSCALE:GREGORIAN 483 | VERSION:2.0 484 | BEGIN:VEVENT 485 | CREATED:20210423T191740Z 486 | DTSTAMP:20210423T192027Z 487 | LAST-MODIFIED:20210423T192027Z 488 | SEQUENCE:3 489 | UID:f09058a5-8d12-454a-86e0-223c2081a798 490 | DTSTART;TZID=America/Indiana/Indianapolis:20210429T153000 491 | DTEND;TZID=America/Indiana/Indianapolis:20210429T163000 492 | SUMMARY:Eric's Intro to R livestream! 493 | DESCRIPTION:Streaming to Twitch: https://www.twitch.tv/rpodcast 494 | END:VEVENT 495 | BEGIN:VTIMEZONE 496 | TZID:America/Indiana/Indianapolis 497 | BEGIN:DAYLIGHT 498 | TZOFFSETFROM:-0500 499 | TZOFFSETTO:-0400 500 | TZNAME:EDT 501 | DTSTART:19700308T020000 502 | RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU 503 | END:DAYLIGHT 504 | BEGIN:STANDARD 505 | TZOFFSETFROM:-0400 506 | TZOFFSETTO:-0500 507 | TZNAME:EST 508 | DTSTART:19701101T020000 509 | RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU 510 | END:STANDARD 511 | END:VTIMEZONE 512 | END:VCALENDAR BEGIN:VCALENDAR 513 | CALSCALE:GREGORIAN 514 | VERSION:2.0 515 | PRODID:-//IDN nextcloud.com//Calendar app 2.2.0//EN 516 | BEGIN:VEVENT 517 | CREATED:20210510T075724Z 518 | DTSTAMP:20210510T195951Z 519 | LAST-MODIFIED:20210510T195951Z 520 | SEQUENCE:4 521 | UID:3b88e1b7-5860-4598-939e-b30bce3c9374 522 | DTSTART;TZID=Europe/Zurich:20210513T220000 523 | DTEND;TZID=Europe/Zurich:20210513T230000 524 | SUMMARY:Software Boutique 2.0\, part 4 525 | LOCATION:YouTube 526 | DESCRIPTION:Let's bring the curated apps in ! 527 | END:VEVENT 528 | BEGIN:VTIMEZONE 529 | TZID:Europe/Zurich 530 | BEGIN:DAYLIGHT 531 | TZOFFSETFROM:+0100 532 | TZOFFSETTO:+0200 533 | TZNAME:CEST 534 | DTSTART:19700329T020000 535 | RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU 536 | END:DAYLIGHT 537 | BEGIN:STANDARD 538 | TZOFFSETFROM:+0200 539 | TZOFFSETTO:+0100 540 | TZNAME:CET 541 | DTSTART:19701025T030000 542 | RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU 543 | END:STANDARD 544 | END:VTIMEZONE 545 | END:VCALENDAR BEGIN:VCALENDAR 546 | CALSCALE:GREGORIAN 547 | VERSION:2.0 548 | PRODID:DAVx5/3.3.10-ose ical4j/3.0.24 549 | BEGIN:VEVENT 550 | DTSTAMP:20210603T183630Z 551 | UID:e6bac299-4e26-47ba-84c9-18de61c96ff0 552 | SUMMARY:Eric's shiny dev series livestream on twitch 553 | DTSTART;TZID=America/New_York:20210608T153000 554 | DTEND;TZID=America/New_York:20210608T163000 555 | SEQUENCE:1 556 | LAST-MODIFIED:20210603T183630Z 557 | END:VEVENT 558 | BEGIN:VTIMEZONE 559 | TZID:America/New_York 560 | BEGIN:DAYLIGHT 561 | TZOFFSETFROM:-0500 562 | TZOFFSETTO:-0400 563 | TZNAME:EDT 564 | DTSTART:19700308T020000 565 | RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU 566 | END:DAYLIGHT 567 | BEGIN:STANDARD 568 | TZOFFSETFROM:-0400 569 | TZOFFSETTO:-0500 570 | TZNAME:EST 571 | DTSTART:19701101T020000 572 | RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU 573 | END:STANDARD 574 | END:VTIMEZONE 575 | END:VCALENDAR -------------------------------------------------------------------------------- /renv/activate.R: -------------------------------------------------------------------------------- 1 | 2 | local({ 3 | 4 | # the requested version of renv 5 | version <- "0.13.2" 6 | 7 | # the project directory 8 | project <- getwd() 9 | 10 | # avoid recursion 11 | if (!is.na(Sys.getenv("RENV_R_INITIALIZING", unset = NA))) 12 | return(invisible(TRUE)) 13 | 14 | # signal that we're loading renv during R startup 15 | Sys.setenv("RENV_R_INITIALIZING" = "true") 16 | on.exit(Sys.unsetenv("RENV_R_INITIALIZING"), add = TRUE) 17 | 18 | # signal that we've consented to use renv 19 | options(renv.consent = TRUE) 20 | 21 | # load the 'utils' package eagerly -- this ensures that renv shims, which 22 | # mask 'utils' packages, will come first on the search path 23 | library(utils, lib.loc = .Library) 24 | 25 | # check to see if renv has already been loaded 26 | if ("renv" %in% loadedNamespaces()) { 27 | 28 | # if renv has already been loaded, and it's the requested version of renv, 29 | # nothing to do 30 | spec <- .getNamespaceInfo(.getNamespace("renv"), "spec") 31 | if (identical(spec[["version"]], version)) 32 | return(invisible(TRUE)) 33 | 34 | # otherwise, unload and attempt to load the correct version of renv 35 | unloadNamespace("renv") 36 | 37 | } 38 | 39 | # load bootstrap tools 40 | bootstrap <- function(version, library) { 41 | 42 | # attempt to download renv 43 | tarball <- tryCatch(renv_bootstrap_download(version), error = identity) 44 | if (inherits(tarball, "error")) 45 | stop("failed to download renv ", version) 46 | 47 | # now attempt to install 48 | status <- tryCatch(renv_bootstrap_install(version, tarball, library), error = identity) 49 | if (inherits(status, "error")) 50 | stop("failed to install renv ", version) 51 | 52 | } 53 | 54 | renv_bootstrap_tests_running <- function() { 55 | getOption("renv.tests.running", default = FALSE) 56 | } 57 | 58 | renv_bootstrap_repos <- function() { 59 | 60 | # check for repos override 61 | repos <- Sys.getenv("RENV_CONFIG_REPOS_OVERRIDE", unset = NA) 62 | if (!is.na(repos)) 63 | return(repos) 64 | 65 | # if we're testing, re-use the test repositories 66 | if (renv_bootstrap_tests_running()) 67 | return(getOption("renv.tests.repos")) 68 | 69 | # retrieve current repos 70 | repos <- getOption("repos") 71 | 72 | # ensure @CRAN@ entries are resolved 73 | repos[repos == "@CRAN@"] <- getOption( 74 | "renv.repos.cran", 75 | "https://cloud.r-project.org" 76 | ) 77 | 78 | # add in renv.bootstrap.repos if set 79 | default <- c(FALLBACK = "https://cloud.r-project.org") 80 | extra <- getOption("renv.bootstrap.repos", default = default) 81 | repos <- c(repos, extra) 82 | 83 | # remove duplicates that might've snuck in 84 | dupes <- duplicated(repos) | duplicated(names(repos)) 85 | repos[!dupes] 86 | 87 | } 88 | 89 | renv_bootstrap_download <- function(version) { 90 | 91 | # if the renv version number has 4 components, assume it must 92 | # be retrieved via github 93 | nv <- numeric_version(version) 94 | components <- unclass(nv)[[1]] 95 | 96 | methods <- if (length(components) == 4L) { 97 | list( 98 | renv_bootstrap_download_github 99 | ) 100 | } else { 101 | list( 102 | renv_bootstrap_download_cran_latest, 103 | renv_bootstrap_download_cran_archive 104 | ) 105 | } 106 | 107 | for (method in methods) { 108 | path <- tryCatch(method(version), error = identity) 109 | if (is.character(path) && file.exists(path)) 110 | return(path) 111 | } 112 | 113 | stop("failed to download renv ", version) 114 | 115 | } 116 | 117 | renv_bootstrap_download_impl <- function(url, destfile) { 118 | 119 | mode <- "wb" 120 | 121 | # https://bugs.r-project.org/bugzilla/show_bug.cgi?id=17715 122 | fixup <- 123 | Sys.info()[["sysname"]] == "Windows" && 124 | substring(url, 1L, 5L) == "file:" 125 | 126 | if (fixup) 127 | mode <- "w+b" 128 | 129 | utils::download.file( 130 | url = url, 131 | destfile = destfile, 132 | mode = mode, 133 | quiet = TRUE 134 | ) 135 | 136 | } 137 | 138 | renv_bootstrap_download_cran_latest <- function(version) { 139 | 140 | spec <- renv_bootstrap_download_cran_latest_find(version) 141 | 142 | message("* Downloading renv ", version, " ... ", appendLF = FALSE) 143 | 144 | type <- spec$type 145 | repos <- spec$repos 146 | 147 | info <- tryCatch( 148 | utils::download.packages( 149 | pkgs = "renv", 150 | destdir = tempdir(), 151 | repos = repos, 152 | type = type, 153 | quiet = TRUE 154 | ), 155 | condition = identity 156 | ) 157 | 158 | if (inherits(info, "condition")) { 159 | message("FAILED") 160 | return(FALSE) 161 | } 162 | 163 | # report success and return 164 | message("OK (downloaded ", type, ")") 165 | info[1, 2] 166 | 167 | } 168 | 169 | renv_bootstrap_download_cran_latest_find <- function(version) { 170 | 171 | # check whether binaries are supported on this system 172 | binary <- 173 | getOption("renv.bootstrap.binary", default = TRUE) && 174 | !identical(.Platform$pkgType, "source") && 175 | !identical(getOption("pkgType"), "source") && 176 | Sys.info()[["sysname"]] %in% c("Darwin", "Windows") 177 | 178 | types <- c(if (binary) "binary", "source") 179 | 180 | # iterate over types + repositories 181 | for (type in types) { 182 | for (repos in renv_bootstrap_repos()) { 183 | 184 | # retrieve package database 185 | db <- tryCatch( 186 | as.data.frame( 187 | utils::available.packages(type = type, repos = repos), 188 | stringsAsFactors = FALSE 189 | ), 190 | error = identity 191 | ) 192 | 193 | if (inherits(db, "error")) 194 | next 195 | 196 | # check for compatible entry 197 | entry <- db[db$Package %in% "renv" & db$Version %in% version, ] 198 | if (nrow(entry) == 0) 199 | next 200 | 201 | # found it; return spec to caller 202 | spec <- list(entry = entry, type = type, repos = repos) 203 | return(spec) 204 | 205 | } 206 | } 207 | 208 | # if we got here, we failed to find renv 209 | fmt <- "renv %s is not available from your declared package repositories" 210 | stop(sprintf(fmt, version)) 211 | 212 | } 213 | 214 | renv_bootstrap_download_cran_archive <- function(version) { 215 | 216 | name <- sprintf("renv_%s.tar.gz", version) 217 | repos <- renv_bootstrap_repos() 218 | urls <- file.path(repos, "src/contrib/Archive/renv", name) 219 | destfile <- file.path(tempdir(), name) 220 | 221 | message("* Downloading renv ", version, " ... ", appendLF = FALSE) 222 | 223 | for (url in urls) { 224 | 225 | status <- tryCatch( 226 | renv_bootstrap_download_impl(url, destfile), 227 | condition = identity 228 | ) 229 | 230 | if (identical(status, 0L)) { 231 | message("OK") 232 | return(destfile) 233 | } 234 | 235 | } 236 | 237 | message("FAILED") 238 | return(FALSE) 239 | 240 | } 241 | 242 | renv_bootstrap_download_github <- function(version) { 243 | 244 | enabled <- Sys.getenv("RENV_BOOTSTRAP_FROM_GITHUB", unset = "TRUE") 245 | if (!identical(enabled, "TRUE")) 246 | return(FALSE) 247 | 248 | # prepare download options 249 | pat <- Sys.getenv("GITHUB_PAT") 250 | if (nzchar(Sys.which("curl")) && nzchar(pat)) { 251 | fmt <- "--location --fail --header \"Authorization: token %s\"" 252 | extra <- sprintf(fmt, pat) 253 | saved <- options("download.file.method", "download.file.extra") 254 | options(download.file.method = "curl", download.file.extra = extra) 255 | on.exit(do.call(base::options, saved), add = TRUE) 256 | } else if (nzchar(Sys.which("wget")) && nzchar(pat)) { 257 | fmt <- "--header=\"Authorization: token %s\"" 258 | extra <- sprintf(fmt, pat) 259 | saved <- options("download.file.method", "download.file.extra") 260 | options(download.file.method = "wget", download.file.extra = extra) 261 | on.exit(do.call(base::options, saved), add = TRUE) 262 | } 263 | 264 | message("* Downloading renv ", version, " from GitHub ... ", appendLF = FALSE) 265 | 266 | url <- file.path("https://api.github.com/repos/rstudio/renv/tarball", version) 267 | name <- sprintf("renv_%s.tar.gz", version) 268 | destfile <- file.path(tempdir(), name) 269 | 270 | status <- tryCatch( 271 | renv_bootstrap_download_impl(url, destfile), 272 | condition = identity 273 | ) 274 | 275 | if (!identical(status, 0L)) { 276 | message("FAILED") 277 | return(FALSE) 278 | } 279 | 280 | message("OK") 281 | return(destfile) 282 | 283 | } 284 | 285 | renv_bootstrap_install <- function(version, tarball, library) { 286 | 287 | # attempt to install it into project library 288 | message("* Installing renv ", version, " ... ", appendLF = FALSE) 289 | dir.create(library, showWarnings = FALSE, recursive = TRUE) 290 | 291 | # invoke using system2 so we can capture and report output 292 | bin <- R.home("bin") 293 | exe <- if (Sys.info()[["sysname"]] == "Windows") "R.exe" else "R" 294 | r <- file.path(bin, exe) 295 | args <- c("--vanilla", "CMD", "INSTALL", "-l", shQuote(library), shQuote(tarball)) 296 | output <- system2(r, args, stdout = TRUE, stderr = TRUE) 297 | message("Done!") 298 | 299 | # check for successful install 300 | status <- attr(output, "status") 301 | if (is.numeric(status) && !identical(status, 0L)) { 302 | header <- "Error installing renv:" 303 | lines <- paste(rep.int("=", nchar(header)), collapse = "") 304 | text <- c(header, lines, output) 305 | writeLines(text, con = stderr()) 306 | } 307 | 308 | status 309 | 310 | } 311 | 312 | renv_bootstrap_platform_prefix <- function() { 313 | 314 | # construct version prefix 315 | version <- paste(R.version$major, R.version$minor, sep = ".") 316 | prefix <- paste("R", numeric_version(version)[1, 1:2], sep = "-") 317 | 318 | # include SVN revision for development versions of R 319 | # (to avoid sharing platform-specific artefacts with released versions of R) 320 | devel <- 321 | identical(R.version[["status"]], "Under development (unstable)") || 322 | identical(R.version[["nickname"]], "Unsuffered Consequences") 323 | 324 | if (devel) 325 | prefix <- paste(prefix, R.version[["svn rev"]], sep = "-r") 326 | 327 | # build list of path components 328 | components <- c(prefix, R.version$platform) 329 | 330 | # include prefix if provided by user 331 | prefix <- renv_bootstrap_platform_prefix_impl() 332 | if (!is.na(prefix) && nzchar(prefix)) 333 | components <- c(prefix, components) 334 | 335 | # build prefix 336 | paste(components, collapse = "/") 337 | 338 | } 339 | 340 | renv_bootstrap_platform_prefix_impl <- function() { 341 | 342 | # if an explicit prefix has been supplied, use it 343 | prefix <- Sys.getenv("RENV_PATHS_PREFIX", unset = NA) 344 | if (!is.na(prefix)) 345 | return(prefix) 346 | 347 | # if the user has requested an automatic prefix, generate it 348 | auto <- Sys.getenv("RENV_PATHS_PREFIX_AUTO", unset = NA) 349 | if (auto %in% c("TRUE", "True", "true", "1")) 350 | return(renv_bootstrap_platform_prefix_auto()) 351 | 352 | # empty string on failure 353 | "" 354 | 355 | } 356 | 357 | renv_bootstrap_platform_prefix_auto <- function() { 358 | 359 | prefix <- tryCatch(renv_bootstrap_platform_os(), error = identity) 360 | if (inherits(prefix, "error") || prefix %in% "unknown") { 361 | 362 | msg <- paste( 363 | "failed to infer current operating system", 364 | "please file a bug report at https://github.com/rstudio/renv/issues", 365 | sep = "; " 366 | ) 367 | 368 | warning(msg) 369 | 370 | } 371 | 372 | prefix 373 | 374 | } 375 | 376 | renv_bootstrap_platform_os <- function() { 377 | 378 | sysinfo <- Sys.info() 379 | sysname <- sysinfo[["sysname"]] 380 | 381 | # handle Windows + macOS up front 382 | if (sysname == "Windows") 383 | return("windows") 384 | else if (sysname == "Darwin") 385 | return("macos") 386 | 387 | # check for os-release files 388 | for (file in c("/etc/os-release", "/usr/lib/os-release")) 389 | if (file.exists(file)) 390 | return(renv_bootstrap_platform_os_via_os_release(file, sysinfo)) 391 | 392 | # check for redhat-release files 393 | if (file.exists("/etc/redhat-release")) 394 | return(renv_bootstrap_platform_os_via_redhat_release()) 395 | 396 | "unknown" 397 | 398 | } 399 | 400 | renv_bootstrap_platform_os_via_os_release <- function(file, sysinfo) { 401 | 402 | # read /etc/os-release 403 | release <- utils::read.table( 404 | file = file, 405 | sep = "=", 406 | quote = c("\"", "'"), 407 | col.names = c("Key", "Value"), 408 | comment.char = "#", 409 | stringsAsFactors = FALSE 410 | ) 411 | 412 | vars <- as.list(release$Value) 413 | names(vars) <- release$Key 414 | 415 | # get os name 416 | os <- tolower(sysinfo[["sysname"]]) 417 | 418 | # read id 419 | id <- "unknown" 420 | for (field in c("ID", "ID_LIKE")) { 421 | if (field %in% names(vars) && nzchar(vars[[field]])) { 422 | id <- vars[[field]] 423 | break 424 | } 425 | } 426 | 427 | # read version 428 | version <- "unknown" 429 | for (field in c("UBUNTU_CODENAME", "VERSION_CODENAME", "VERSION_ID", "BUILD_ID")) { 430 | if (field %in% names(vars) && nzchar(vars[[field]])) { 431 | version <- vars[[field]] 432 | break 433 | } 434 | } 435 | 436 | # join together 437 | paste(c(os, id, version), collapse = "-") 438 | 439 | } 440 | 441 | renv_bootstrap_platform_os_via_redhat_release <- function() { 442 | 443 | # read /etc/redhat-release 444 | contents <- readLines("/etc/redhat-release", warn = FALSE) 445 | 446 | # infer id 447 | id <- if (grepl("centos", contents, ignore.case = TRUE)) 448 | "centos" 449 | else if (grepl("redhat", contents, ignore.case = TRUE)) 450 | "redhat" 451 | else 452 | "unknown" 453 | 454 | # try to find a version component (very hacky) 455 | version <- "unknown" 456 | 457 | parts <- strsplit(contents, "[[:space:]]")[[1L]] 458 | for (part in parts) { 459 | 460 | nv <- tryCatch(numeric_version(part), error = identity) 461 | if (inherits(nv, "error")) 462 | next 463 | 464 | version <- nv[1, 1] 465 | break 466 | 467 | } 468 | 469 | paste(c("linux", id, version), collapse = "-") 470 | 471 | } 472 | 473 | renv_bootstrap_library_root_name <- function(project) { 474 | 475 | # use project name as-is if requested 476 | asis <- Sys.getenv("RENV_PATHS_LIBRARY_ROOT_ASIS", unset = "FALSE") 477 | if (asis) 478 | return(basename(project)) 479 | 480 | # otherwise, disambiguate based on project's path 481 | id <- substring(renv_bootstrap_hash_text(project), 1L, 8L) 482 | paste(basename(project), id, sep = "-") 483 | 484 | } 485 | 486 | renv_bootstrap_library_root <- function(project) { 487 | 488 | path <- Sys.getenv("RENV_PATHS_LIBRARY", unset = NA) 489 | if (!is.na(path)) 490 | return(path) 491 | 492 | path <- Sys.getenv("RENV_PATHS_LIBRARY_ROOT", unset = NA) 493 | if (!is.na(path)) { 494 | name <- renv_bootstrap_library_root_name(project) 495 | return(file.path(path, name)) 496 | } 497 | 498 | prefix <- renv_bootstrap_profile_prefix() 499 | paste(c(project, prefix, "renv/library"), collapse = "/") 500 | 501 | } 502 | 503 | renv_bootstrap_validate_version <- function(version) { 504 | 505 | loadedversion <- utils::packageDescription("renv", fields = "Version") 506 | if (version == loadedversion) 507 | return(TRUE) 508 | 509 | # assume four-component versions are from GitHub; three-component 510 | # versions are from CRAN 511 | components <- strsplit(loadedversion, "[.-]")[[1]] 512 | remote <- if (length(components) == 4L) 513 | paste("rstudio/renv", loadedversion, sep = "@") 514 | else 515 | paste("renv", loadedversion, sep = "@") 516 | 517 | fmt <- paste( 518 | "renv %1$s was loaded from project library, but this project is configured to use renv %2$s.", 519 | "Use `renv::record(\"%3$s\")` to record renv %1$s in the lockfile.", 520 | "Use `renv::restore(packages = \"renv\")` to install renv %2$s into the project library.", 521 | sep = "\n" 522 | ) 523 | 524 | msg <- sprintf(fmt, loadedversion, version, remote) 525 | warning(msg, call. = FALSE) 526 | 527 | FALSE 528 | 529 | } 530 | 531 | renv_bootstrap_hash_text <- function(text) { 532 | 533 | hashfile <- tempfile("renv-hash-") 534 | on.exit(unlink(hashfile), add = TRUE) 535 | 536 | writeLines(text, con = hashfile) 537 | tools::md5sum(hashfile) 538 | 539 | } 540 | 541 | renv_bootstrap_load <- function(project, libpath, version) { 542 | 543 | # try to load renv from the project library 544 | if (!requireNamespace("renv", lib.loc = libpath, quietly = TRUE)) 545 | return(FALSE) 546 | 547 | # warn if the version of renv loaded does not match 548 | renv_bootstrap_validate_version(version) 549 | 550 | # load the project 551 | renv::load(project) 552 | 553 | TRUE 554 | 555 | } 556 | 557 | renv_bootstrap_profile_load <- function(project) { 558 | 559 | # if RENV_PROFILE is already set, just use that 560 | profile <- Sys.getenv("RENV_PROFILE", unset = NA) 561 | if (!is.na(profile) && nzchar(profile)) 562 | return(profile) 563 | 564 | # check for a profile file (nothing to do if it doesn't exist) 565 | path <- file.path(project, "renv/local/profile") 566 | if (!file.exists(path)) 567 | return(NULL) 568 | 569 | # read the profile, and set it if it exists 570 | contents <- readLines(path, warn = FALSE) 571 | if (length(contents) == 0L) 572 | return(NULL) 573 | 574 | # set RENV_PROFILE 575 | profile <- contents[[1L]] 576 | if (nzchar(profile)) 577 | Sys.setenv(RENV_PROFILE = profile) 578 | 579 | profile 580 | 581 | } 582 | 583 | renv_bootstrap_profile_prefix <- function() { 584 | profile <- renv_bootstrap_profile_get() 585 | if (!is.null(profile)) 586 | return(file.path("renv/profiles", profile)) 587 | } 588 | 589 | renv_bootstrap_profile_get <- function() { 590 | profile <- Sys.getenv("RENV_PROFILE", unset = "") 591 | renv_bootstrap_profile_normalize(profile) 592 | } 593 | 594 | renv_bootstrap_profile_set <- function(profile) { 595 | profile <- renv_bootstrap_profile_normalize(profile) 596 | if (is.null(profile)) 597 | Sys.unsetenv("RENV_PROFILE") 598 | else 599 | Sys.setenv(RENV_PROFILE = profile) 600 | } 601 | 602 | renv_bootstrap_profile_normalize <- function(profile) { 603 | 604 | if (is.null(profile) || profile %in% c("", "default")) 605 | return(NULL) 606 | 607 | profile 608 | 609 | } 610 | 611 | # load the renv profile, if any 612 | renv_bootstrap_profile_load(project) 613 | 614 | # construct path to library root 615 | root <- renv_bootstrap_library_root(project) 616 | 617 | # construct library prefix for platform 618 | prefix <- renv_bootstrap_platform_prefix() 619 | 620 | # construct full libpath 621 | libpath <- file.path(root, prefix) 622 | 623 | # attempt to load 624 | if (renv_bootstrap_load(project, libpath, version)) 625 | return(TRUE) 626 | 627 | # load failed; inform user we're about to bootstrap 628 | prefix <- paste("# Bootstrapping renv", version) 629 | postfix <- paste(rep.int("-", 77L - nchar(prefix)), collapse = "") 630 | header <- paste(prefix, postfix) 631 | message(header) 632 | 633 | # perform bootstrap 634 | bootstrap(version, libpath) 635 | 636 | # exit early if we're just testing bootstrap 637 | if (!is.na(Sys.getenv("RENV_BOOTSTRAP_INSTALL_ONLY", unset = NA))) 638 | return(TRUE) 639 | 640 | # try again to load 641 | if (requireNamespace("renv", lib.loc = libpath, quietly = TRUE)) { 642 | message("* Successfully installed and loaded renv ", version, ".") 643 | return(renv::load()) 644 | } 645 | 646 | # failed to download or load renv; warn the user 647 | msg <- c( 648 | "Failed to find an renv installation: the project will not be loaded.", 649 | "Use `renv::activate()` to re-initialize the project." 650 | ) 651 | 652 | warning(paste(msg, collapse = "\n"), call. = FALSE) 653 | 654 | }) 655 | -------------------------------------------------------------------------------- /renv.lock: -------------------------------------------------------------------------------- 1 | { 2 | "R": { 3 | "Version": "4.1.0", 4 | "Repositories": [ 5 | { 6 | "Name": "CRAN", 7 | "URL": "https://packagemanager.rstudio.com/all/latest" 8 | } 9 | ] 10 | }, 11 | "Packages": { 12 | "BH": { 13 | "Package": "BH", 14 | "Version": "1.75.0-0", 15 | "Source": "Repository", 16 | "Repository": "RSPM", 17 | "Hash": "e4c04affc2cac20c8fec18385cd14691" 18 | }, 19 | "DBI": { 20 | "Package": "DBI", 21 | "Version": "1.1.1", 22 | "Source": "Repository", 23 | "Repository": "RSPM", 24 | "Hash": "030aaec5bc6553f35347cbb1e70b1a17" 25 | }, 26 | "MASS": { 27 | "Package": "MASS", 28 | "Version": "7.3-54", 29 | "Source": "Repository", 30 | "Repository": "CRAN", 31 | "Hash": "0e59129db205112e3963904db67fd0dc" 32 | }, 33 | "Matrix": { 34 | "Package": "Matrix", 35 | "Version": "1.3-3", 36 | "Source": "Repository", 37 | "Repository": "CRAN", 38 | "Hash": "df57c82e79600601287edfdcef92c2d6" 39 | }, 40 | "R6": { 41 | "Package": "R6", 42 | "Version": "2.5.0", 43 | "Source": "Repository", 44 | "Repository": "RSPM", 45 | "Hash": "b203113193e70978a696b2809525649d" 46 | }, 47 | "RColorBrewer": { 48 | "Package": "RColorBrewer", 49 | "Version": "1.1-2", 50 | "Source": "Repository", 51 | "Repository": "RSPM", 52 | "Hash": "e031418365a7f7a766181ab5a41a5716" 53 | }, 54 | "RSQLite": { 55 | "Package": "RSQLite", 56 | "Version": "2.2.7", 57 | "Source": "Repository", 58 | "Repository": "RSPM", 59 | "Hash": "71f19d00a7736b24492fb26b483bc450" 60 | }, 61 | "Rcpp": { 62 | "Package": "Rcpp", 63 | "Version": "1.0.7", 64 | "Source": "Repository", 65 | "Repository": "RSPM", 66 | "Hash": "dab19adae4440ae55aa8a9d238b246bb" 67 | }, 68 | "RcppCCTZ": { 69 | "Package": "RcppCCTZ", 70 | "Version": "0.2.9", 71 | "Source": "Repository", 72 | "Repository": "RSPM", 73 | "Hash": "b0826082b7ae7063e42d06856afed2d6" 74 | }, 75 | "RcppDate": { 76 | "Package": "RcppDate", 77 | "Version": "0.0.3", 78 | "Source": "Repository", 79 | "Repository": "RSPM", 80 | "Hash": "08cc427d6fe7a63e604cfa11aad31006" 81 | }, 82 | "anytime": { 83 | "Package": "anytime", 84 | "Version": "0.3.9", 85 | "Source": "Repository", 86 | "Repository": "RSPM", 87 | "Hash": "74a64813f17b492da9c6afda6b128e3d" 88 | }, 89 | "askpass": { 90 | "Package": "askpass", 91 | "Version": "1.1", 92 | "Source": "Repository", 93 | "Repository": "RSPM", 94 | "Hash": "e8a22846fff485f0be3770c2da758713" 95 | }, 96 | "attempt": { 97 | "Package": "attempt", 98 | "Version": "0.3.1", 99 | "Source": "Repository", 100 | "Repository": "RSPM", 101 | "Hash": "d7421bb5dfeb2676b9e4a5a60c2fcfd2" 102 | }, 103 | "base64enc": { 104 | "Package": "base64enc", 105 | "Version": "0.1-3", 106 | "Source": "Repository", 107 | "Repository": "RSPM", 108 | "Hash": "543776ae6848fde2f48ff3816d0628bc" 109 | }, 110 | "bit": { 111 | "Package": "bit", 112 | "Version": "4.0.4", 113 | "Source": "Repository", 114 | "Repository": "RSPM", 115 | "Hash": "f36715f14d94678eea9933af927bc15d" 116 | }, 117 | "bit64": { 118 | "Package": "bit64", 119 | "Version": "4.0.5", 120 | "Source": "Repository", 121 | "Repository": "RSPM", 122 | "Hash": "9fe98599ca456d6552421db0d6772d8f" 123 | }, 124 | "blob": { 125 | "Package": "blob", 126 | "Version": "1.2.1", 127 | "Source": "Repository", 128 | "Repository": "RSPM", 129 | "Hash": "9addc7e2c5954eca5719928131fed98c" 130 | }, 131 | "brew": { 132 | "Package": "brew", 133 | "Version": "1.0-6", 134 | "Source": "Repository", 135 | "Repository": "RSPM", 136 | "Hash": "92a5f887f9ae3035ac7afde22ba73ee9" 137 | }, 138 | "brio": { 139 | "Package": "brio", 140 | "Version": "1.1.2", 141 | "Source": "Repository", 142 | "Repository": "RSPM", 143 | "Hash": "2f01e16ff9571fe70381c7b9ae560dc4" 144 | }, 145 | "bslib": { 146 | "Package": "bslib", 147 | "Version": "0.2.5.1", 148 | "Source": "Repository", 149 | "Repository": "RSPM", 150 | "Hash": "2f069f3f42847231aef7baa49bed97b0" 151 | }, 152 | "cachem": { 153 | "Package": "cachem", 154 | "Version": "1.0.5", 155 | "Source": "Repository", 156 | "Repository": "RSPM", 157 | "Hash": "5346f76a33eb7417812c270b04a5581b" 158 | }, 159 | "caldav": { 160 | "Package": "caldav", 161 | "Version": "0.1.0", 162 | "Source": "GitHub", 163 | "RemoteType": "github", 164 | "RemoteHost": "api.github.com", 165 | "RemoteRepo": "caldav", 166 | "RemoteUsername": "petermeissner", 167 | "RemoteRef": "HEAD", 168 | "RemoteSha": "194169af76fda3fe0ae01d36a280190539a079ba", 169 | "Hash": "4a99e12c9a548122f4ca11cd2cf84de2" 170 | }, 171 | "calendar": { 172 | "Package": "calendar", 173 | "Version": "0.0.1", 174 | "Source": "Repository", 175 | "Repository": "RSPM", 176 | "Hash": "ab478f470a5203ff9990e42d916005b0" 177 | }, 178 | "callr": { 179 | "Package": "callr", 180 | "Version": "3.7.0", 181 | "Source": "Repository", 182 | "Repository": "RSPM", 183 | "Hash": "461aa75a11ce2400245190ef5d3995df" 184 | }, 185 | "cli": { 186 | "Package": "cli", 187 | "Version": "2.5.0", 188 | "Source": "Repository", 189 | "Repository": "RSPM", 190 | "Hash": "a94ba44cee3ea571e813721e64184172" 191 | }, 192 | "clipr": { 193 | "Package": "clipr", 194 | "Version": "0.7.1", 195 | "Source": "Repository", 196 | "Repository": "RSPM", 197 | "Hash": "ebaa97ac99cc2daf04e77eecc7b781d7" 198 | }, 199 | "clock": { 200 | "Package": "clock", 201 | "Version": "0.3.0", 202 | "Source": "Repository", 203 | "Repository": "RSPM", 204 | "Hash": "e2e3e46ab3c217313bc77a9eb364d128" 205 | }, 206 | "colorspace": { 207 | "Package": "colorspace", 208 | "Version": "2.0-2", 209 | "Source": "Repository", 210 | "Repository": "RSPM", 211 | "Hash": "6baccb763ee83c0bd313460fdb8b8a84" 212 | }, 213 | "colourpicker": { 214 | "Package": "colourpicker", 215 | "Version": "1.1.0", 216 | "Source": "Repository", 217 | "Repository": "RSPM", 218 | "Hash": "fe0cb3d8854c168aef827aeab4b3b473" 219 | }, 220 | "commonmark": { 221 | "Package": "commonmark", 222 | "Version": "1.7", 223 | "Source": "Repository", 224 | "Repository": "RSPM", 225 | "Hash": "0f22be39ec1d141fd03683c06f3a6e67" 226 | }, 227 | "config": { 228 | "Package": "config", 229 | "Version": "0.3.1", 230 | "Source": "Repository", 231 | "Repository": "RSPM", 232 | "Hash": "31d77b09f63550cee9ecb5a08bf76e8f" 233 | }, 234 | "cpp11": { 235 | "Package": "cpp11", 236 | "Version": "0.2.7", 237 | "Source": "Repository", 238 | "Repository": "RSPM", 239 | "Hash": "730eebcc741a5c36761f7d4d0f5e37b8" 240 | }, 241 | "crayon": { 242 | "Package": "crayon", 243 | "Version": "1.4.1", 244 | "Source": "Repository", 245 | "Repository": "RSPM", 246 | "Hash": "e75525c55c70e5f4f78c9960a4b402e9" 247 | }, 248 | "credentials": { 249 | "Package": "credentials", 250 | "Version": "1.3.0", 251 | "Source": "Repository", 252 | "Repository": "RSPM", 253 | "Hash": "a96728288c75a814c900af9da84387be" 254 | }, 255 | "curl": { 256 | "Package": "curl", 257 | "Version": "4.3.1", 258 | "Source": "Repository", 259 | "Repository": "RSPM", 260 | "Hash": "c5e68405893f030f139f6d6675eac675" 261 | }, 262 | "data.table": { 263 | "Package": "data.table", 264 | "Version": "1.14.0", 265 | "Source": "Repository", 266 | "Repository": "RSPM", 267 | "Hash": "d1b8b1a821ee564a3515fa6c6d5c52dc" 268 | }, 269 | "desc": { 270 | "Package": "desc", 271 | "Version": "1.3.0", 272 | "Source": "Repository", 273 | "Repository": "RSPM", 274 | "Hash": "b6963166f7f10b970af1006c462ce6cd" 275 | }, 276 | "devtools": { 277 | "Package": "devtools", 278 | "Version": "2.4.2", 279 | "Source": "Repository", 280 | "Repository": "RSPM", 281 | "Hash": "048d0b914d2cea61f440d35dd3cbddac" 282 | }, 283 | "diffobj": { 284 | "Package": "diffobj", 285 | "Version": "0.3.4", 286 | "Source": "Repository", 287 | "Repository": "RSPM", 288 | "Hash": "feb5b7455eba422a2c110bb89852e6a3" 289 | }, 290 | "digest": { 291 | "Package": "digest", 292 | "Version": "0.6.27", 293 | "Source": "Repository", 294 | "Repository": "RSPM", 295 | "Hash": "a0cbe758a531d054b537d16dff4d58a1" 296 | }, 297 | "dockerfiler": { 298 | "Package": "dockerfiler", 299 | "Version": "0.1.3", 300 | "Source": "Repository", 301 | "Repository": "RSPM", 302 | "Hash": "95d780d9cf3384d97579fc324a8d4c31" 303 | }, 304 | "dplyr": { 305 | "Package": "dplyr", 306 | "Version": "1.0.6", 307 | "Source": "Repository", 308 | "Repository": "RSPM", 309 | "Hash": "19e84500b64bc7e589cb1e2550e25832" 310 | }, 311 | "ellipsis": { 312 | "Package": "ellipsis", 313 | "Version": "0.3.2", 314 | "Source": "Repository", 315 | "Repository": "RSPM", 316 | "Hash": "bb0eec2fe32e88d9e2836c2f73ea2077" 317 | }, 318 | "evaluate": { 319 | "Package": "evaluate", 320 | "Version": "0.14", 321 | "Source": "Repository", 322 | "Repository": "RSPM", 323 | "Hash": "ec8ca05cffcc70569eaaad8469d2a3a7" 324 | }, 325 | "fansi": { 326 | "Package": "fansi", 327 | "Version": "0.5.0", 328 | "Source": "Repository", 329 | "Repository": "RSPM", 330 | "Hash": "d447b40982c576a72b779f0a3b3da227" 331 | }, 332 | "farver": { 333 | "Package": "farver", 334 | "Version": "2.1.0", 335 | "Source": "Repository", 336 | "Repository": "RSPM", 337 | "Hash": "c98eb5133d9cb9e1622b8691487f11bb" 338 | }, 339 | "fastmap": { 340 | "Package": "fastmap", 341 | "Version": "1.1.0", 342 | "Source": "Repository", 343 | "Repository": "RSPM", 344 | "Hash": "77bd60a6157420d4ffa93b27cf6a58b8" 345 | }, 346 | "fs": { 347 | "Package": "fs", 348 | "Version": "1.5.0", 349 | "Source": "Repository", 350 | "Repository": "RSPM", 351 | "Hash": "44594a07a42e5f91fac9f93fda6d0109" 352 | }, 353 | "generics": { 354 | "Package": "generics", 355 | "Version": "0.1.0", 356 | "Source": "Repository", 357 | "Repository": "RSPM", 358 | "Hash": "4d243a9c10b00589889fe32314ffd902" 359 | }, 360 | "gert": { 361 | "Package": "gert", 362 | "Version": "1.3.0", 363 | "Source": "Repository", 364 | "Repository": "RSPM", 365 | "Hash": "56f398846cd40937be6b435a66bd3f37" 366 | }, 367 | "ggplot2": { 368 | "Package": "ggplot2", 369 | "Version": "3.3.5", 370 | "Source": "Repository", 371 | "Repository": "RSPM", 372 | "Hash": "d7566c471c7b17e095dd023b9ef155ad" 373 | }, 374 | "gh": { 375 | "Package": "gh", 376 | "Version": "1.3.0", 377 | "Source": "Repository", 378 | "Repository": "RSPM", 379 | "Hash": "38c2580abbda249bd6afeec00d14f531" 380 | }, 381 | "gitcreds": { 382 | "Package": "gitcreds", 383 | "Version": "0.1.1", 384 | "Source": "Repository", 385 | "Repository": "RSPM", 386 | "Hash": "f3aefccc1cc50de6338146b62f115de8" 387 | }, 388 | "glue": { 389 | "Package": "glue", 390 | "Version": "1.4.2", 391 | "Source": "Repository", 392 | "Repository": "RSPM", 393 | "Hash": "6efd734b14c6471cfe443345f3e35e29" 394 | }, 395 | "golem": { 396 | "Package": "golem", 397 | "Version": "0.3.1", 398 | "Source": "Repository", 399 | "Repository": "RSPM", 400 | "Hash": "0eaf594de1dcbcd37c6d79806d57b473" 401 | }, 402 | "gtable": { 403 | "Package": "gtable", 404 | "Version": "0.3.0", 405 | "Source": "Repository", 406 | "Repository": "RSPM", 407 | "Hash": "ac5c6baf7822ce8732b343f14c072c4d" 408 | }, 409 | "here": { 410 | "Package": "here", 411 | "Version": "1.0.1", 412 | "Source": "Repository", 413 | "Repository": "RSPM", 414 | "Hash": "24b224366f9c2e7534d2344d10d59211" 415 | }, 416 | "highr": { 417 | "Package": "highr", 418 | "Version": "0.9", 419 | "Source": "Repository", 420 | "Repository": "RSPM", 421 | "Hash": "8eb36c8125038e648e5d111c0d7b2ed4" 422 | }, 423 | "hms": { 424 | "Package": "hms", 425 | "Version": "1.1.0", 426 | "Source": "Repository", 427 | "Repository": "RSPM", 428 | "Hash": "e4bf161ccb74a2c1c0e8ac63bbe332b4" 429 | }, 430 | "htmltools": { 431 | "Package": "htmltools", 432 | "Version": "0.5.1.1", 433 | "Source": "Repository", 434 | "Repository": "RSPM", 435 | "Hash": "af2c2531e55df5cf230c4b5444fc973c" 436 | }, 437 | "htmlwidgets": { 438 | "Package": "htmlwidgets", 439 | "Version": "1.5.3", 440 | "Source": "Repository", 441 | "Repository": "RSPM", 442 | "Hash": "6fdaa86d0700f8b3e92ee3c445a5a10d" 443 | }, 444 | "httpgd": { 445 | "Package": "httpgd", 446 | "Version": "1.1.1", 447 | "Source": "GitHub", 448 | "RemoteType": "github", 449 | "RemoteHost": "api.github.com", 450 | "RemoteUsername": "nx10", 451 | "RemoteRepo": "httpgd", 452 | "RemoteRef": "master", 453 | "RemoteSha": "fd72a3cd03ed4f19a9e903a5e1dcda534aea2bef", 454 | "Hash": "a5e9932fe58d74875b5aadca6e87cbfc" 455 | }, 456 | "httpuv": { 457 | "Package": "httpuv", 458 | "Version": "1.6.1", 459 | "Source": "Repository", 460 | "Repository": "RSPM", 461 | "Hash": "54344a78aae37bc6ef39b1240969df8e" 462 | }, 463 | "httr": { 464 | "Package": "httr", 465 | "Version": "1.4.2", 466 | "Source": "Repository", 467 | "Repository": "RSPM", 468 | "Hash": "a525aba14184fec243f9eaec62fbed43" 469 | }, 470 | "ini": { 471 | "Package": "ini", 472 | "Version": "0.3.1", 473 | "Source": "Repository", 474 | "Repository": "RSPM", 475 | "Hash": "6154ec2223172bce8162d4153cda21f7" 476 | }, 477 | "isoband": { 478 | "Package": "isoband", 479 | "Version": "0.2.5", 480 | "Source": "Repository", 481 | "Repository": "RSPM", 482 | "Hash": "7ab57a6de7f48a8dc84910d1eca42883" 483 | }, 484 | "jquerylib": { 485 | "Package": "jquerylib", 486 | "Version": "0.1.4", 487 | "Source": "Repository", 488 | "Repository": "RSPM", 489 | "Hash": "5aab57a3bd297eee1c1d862735972182" 490 | }, 491 | "jsonlite": { 492 | "Package": "jsonlite", 493 | "Version": "1.7.2", 494 | "Source": "Repository", 495 | "Repository": "RSPM", 496 | "Hash": "98138e0994d41508c7a6b84a0600cfcb" 497 | }, 498 | "knitr": { 499 | "Package": "knitr", 500 | "Version": "1.33", 501 | "Source": "Repository", 502 | "Repository": "RSPM", 503 | "Hash": "0bc1b5da1b0eb07cd4b727e95e9ff0b8" 504 | }, 505 | "labeling": { 506 | "Package": "labeling", 507 | "Version": "0.4.2", 508 | "Source": "Repository", 509 | "Repository": "RSPM", 510 | "Hash": "3d5108641f47470611a32d0bdf357a72" 511 | }, 512 | "later": { 513 | "Package": "later", 514 | "Version": "1.2.0", 515 | "Source": "Repository", 516 | "Repository": "RSPM", 517 | "Hash": "b61890ae77fea19fc8acadd25db70aa4" 518 | }, 519 | "lattice": { 520 | "Package": "lattice", 521 | "Version": "0.20-44", 522 | "Source": "Repository", 523 | "Repository": "CRAN", 524 | "Hash": "f36bf1a849d9106dc2af72e501f9de41" 525 | }, 526 | "lifecycle": { 527 | "Package": "lifecycle", 528 | "Version": "1.0.0", 529 | "Source": "Repository", 530 | "Repository": "RSPM", 531 | "Hash": "3471fb65971f1a7b2d4ae7848cf2db8d" 532 | }, 533 | "lubridate": { 534 | "Package": "lubridate", 535 | "Version": "1.7.10", 536 | "Source": "Repository", 537 | "Repository": "RSPM", 538 | "Hash": "1ebfdc8a3cfe8fe19184f5481972b092" 539 | }, 540 | "magrittr": { 541 | "Package": "magrittr", 542 | "Version": "2.0.1", 543 | "Source": "Repository", 544 | "Repository": "RSPM", 545 | "Hash": "41287f1ac7d28a92f0a286ed507928d3" 546 | }, 547 | "markdown": { 548 | "Package": "markdown", 549 | "Version": "1.1", 550 | "Source": "Repository", 551 | "Repository": "RSPM", 552 | "Hash": "61e4a10781dd00d7d81dd06ca9b94e95" 553 | }, 554 | "memoise": { 555 | "Package": "memoise", 556 | "Version": "2.0.0", 557 | "Source": "Repository", 558 | "Repository": "RSPM", 559 | "Hash": "a0bc51650201a56d00a4798523cc91b3" 560 | }, 561 | "mgcv": { 562 | "Package": "mgcv", 563 | "Version": "1.8-35", 564 | "Source": "Repository", 565 | "Repository": "CRAN", 566 | "Hash": "89fd8b2ad4a6cb4979b78cf2a77ab503" 567 | }, 568 | "mime": { 569 | "Package": "mime", 570 | "Version": "0.11", 571 | "Source": "Repository", 572 | "Repository": "RSPM", 573 | "Hash": "8974a907200fc9948d636fe7d85ca9fb" 574 | }, 575 | "miniUI": { 576 | "Package": "miniUI", 577 | "Version": "0.1.1.1", 578 | "Source": "Repository", 579 | "Repository": "RSPM", 580 | "Hash": "fec5f52652d60615fdb3957b3d74324a" 581 | }, 582 | "munsell": { 583 | "Package": "munsell", 584 | "Version": "0.5.0", 585 | "Source": "Repository", 586 | "Repository": "RSPM", 587 | "Hash": "6dfe8bf774944bd5595785e3229d8771" 588 | }, 589 | "nanotime": { 590 | "Package": "nanotime", 591 | "Version": "0.3.2", 592 | "Source": "Repository", 593 | "Repository": "RSPM", 594 | "Hash": "2ed5b70ae237d914d5b790ed09e34146" 595 | }, 596 | "nlme": { 597 | "Package": "nlme", 598 | "Version": "3.1-152", 599 | "Source": "Repository", 600 | "Repository": "CRAN", 601 | "Hash": "35de1ce639f20b5e10f7f46260730c65" 602 | }, 603 | "openssl": { 604 | "Package": "openssl", 605 | "Version": "1.4.4", 606 | "Source": "Repository", 607 | "Repository": "RSPM", 608 | "Hash": "f4dbc5a47fd93d3415249884d31d6791" 609 | }, 610 | "packrat": { 611 | "Package": "packrat", 612 | "Version": "0.6.0", 613 | "Source": "Repository", 614 | "Repository": "RSPM", 615 | "Hash": "0d6cc4c357e7602bb3eee299f4cfc2a5" 616 | }, 617 | "pillar": { 618 | "Package": "pillar", 619 | "Version": "1.6.1", 620 | "Source": "Repository", 621 | "Repository": "RSPM", 622 | "Hash": "8672ae02bd20f6479bce2d06c7ff1401" 623 | }, 624 | "pkgbuild": { 625 | "Package": "pkgbuild", 626 | "Version": "1.2.0", 627 | "Source": "Repository", 628 | "Repository": "RSPM", 629 | "Hash": "725fcc30222d4d11ec68efb8ff11a9af" 630 | }, 631 | "pkgconfig": { 632 | "Package": "pkgconfig", 633 | "Version": "2.0.3", 634 | "Source": "Repository", 635 | "Repository": "RSPM", 636 | "Hash": "01f28d4278f15c76cddbea05899c5d6f" 637 | }, 638 | "pkgload": { 639 | "Package": "pkgload", 640 | "Version": "1.2.1", 641 | "Source": "Repository", 642 | "Repository": "RSPM", 643 | "Hash": "463642747f81879e6752485aefb831cf" 644 | }, 645 | "plogr": { 646 | "Package": "plogr", 647 | "Version": "0.2.0", 648 | "Source": "Repository", 649 | "Repository": "RSPM", 650 | "Hash": "09eb987710984fc2905c7129c7d85e65" 651 | }, 652 | "png": { 653 | "Package": "png", 654 | "Version": "0.1-7", 655 | "Source": "Repository", 656 | "Repository": "RSPM", 657 | "Hash": "03b7076c234cb3331288919983326c55" 658 | }, 659 | "praise": { 660 | "Package": "praise", 661 | "Version": "1.0.0", 662 | "Source": "Repository", 663 | "Repository": "RSPM", 664 | "Hash": "a555924add98c99d2f411e37e7d25e9f" 665 | }, 666 | "prettyunits": { 667 | "Package": "prettyunits", 668 | "Version": "1.1.1", 669 | "Source": "Repository", 670 | "Repository": "RSPM", 671 | "Hash": "95ef9167b75dde9d2ccc3c7528393e7e" 672 | }, 673 | "processx": { 674 | "Package": "processx", 675 | "Version": "3.5.2", 676 | "Source": "Repository", 677 | "Repository": "RSPM", 678 | "Hash": "0cbca2bc4d16525d009c4dbba156b37c" 679 | }, 680 | "progress": { 681 | "Package": "progress", 682 | "Version": "1.2.2", 683 | "Source": "Repository", 684 | "Repository": "RSPM", 685 | "Hash": "14dc9f7a3c91ebb14ec5bb9208a07061" 686 | }, 687 | "promises": { 688 | "Package": "promises", 689 | "Version": "1.2.0.1", 690 | "Source": "Repository", 691 | "Repository": "RSPM", 692 | "Hash": "4ab2c43adb4d4699cf3690acd378d75d" 693 | }, 694 | "ps": { 695 | "Package": "ps", 696 | "Version": "1.6.0", 697 | "Source": "Repository", 698 | "Repository": "RSPM", 699 | "Hash": "32620e2001c1dce1af49c49dccbb9420" 700 | }, 701 | "purrr": { 702 | "Package": "purrr", 703 | "Version": "0.3.4", 704 | "Source": "Repository", 705 | "Repository": "RSPM", 706 | "Hash": "97def703420c8ab10d8f0e6c72101e02" 707 | }, 708 | "rappdirs": { 709 | "Package": "rappdirs", 710 | "Version": "0.3.3", 711 | "Source": "Repository", 712 | "Repository": "RSPM", 713 | "Hash": "5e3c5dc0b071b21fa128676560dbe94d" 714 | }, 715 | "rcmdcheck": { 716 | "Package": "rcmdcheck", 717 | "Version": "1.3.3", 718 | "Source": "Repository", 719 | "Repository": "RSPM", 720 | "Hash": "ed95895886dab6d2a584da45503555da" 721 | }, 722 | "readr": { 723 | "Package": "readr", 724 | "Version": "2.0.0", 725 | "Source": "Repository", 726 | "Repository": "RSPM", 727 | "Hash": "849038f0839134ab35e719a9820005a6" 728 | }, 729 | "rematch2": { 730 | "Package": "rematch2", 731 | "Version": "2.1.2", 732 | "Source": "Repository", 733 | "Repository": "RSPM", 734 | "Hash": "76c9e04c712a05848ae7a23d2f170a40" 735 | }, 736 | "remotes": { 737 | "Package": "remotes", 738 | "Version": "2.4.0", 739 | "Source": "Repository", 740 | "Repository": "RSPM", 741 | "Hash": "a85ebb35721573b196317b49ddd2dfe4" 742 | }, 743 | "renv": { 744 | "Package": "renv", 745 | "Version": "0.13.2", 746 | "Source": "Repository", 747 | "Repository": "RSPM", 748 | "Hash": "079cb1f03ff972b30401ed05623cbe92" 749 | }, 750 | "reticulate": { 751 | "Package": "reticulate", 752 | "Version": "1.20", 753 | "Source": "Repository", 754 | "Repository": "RSPM", 755 | "Hash": "30ab0ea8c8d3dd16a3fa06903449bbfb" 756 | }, 757 | "rlang": { 758 | "Package": "rlang", 759 | "Version": "0.4.11", 760 | "Source": "Repository", 761 | "Repository": "RSPM", 762 | "Hash": "515f341d3affe0de9e4a7f762efb0456" 763 | }, 764 | "rmarkdown": { 765 | "Package": "rmarkdown", 766 | "Version": "2.8", 767 | "Source": "Repository", 768 | "Repository": "RSPM", 769 | "Hash": "f518ba47713f92d0d603eec7c6888faf" 770 | }, 771 | "roxygen2": { 772 | "Package": "roxygen2", 773 | "Version": "7.1.1", 774 | "Source": "Repository", 775 | "Repository": "RSPM", 776 | "Hash": "fcd94e00cc409b25d07ca50f7bf339f5" 777 | }, 778 | "rprojroot": { 779 | "Package": "rprojroot", 780 | "Version": "2.0.2", 781 | "Source": "Repository", 782 | "Repository": "RSPM", 783 | "Hash": "249d8cd1e74a8f6a26194a91b47f21d1" 784 | }, 785 | "rsconnect": { 786 | "Package": "rsconnect", 787 | "Version": "0.8.24", 788 | "Source": "Repository", 789 | "Repository": "RSPM", 790 | "Hash": "5e21fd77eb844fa1ff1d23cb04e1d753" 791 | }, 792 | "rstudioapi": { 793 | "Package": "rstudioapi", 794 | "Version": "0.13", 795 | "Source": "Repository", 796 | "Repository": "RSPM", 797 | "Hash": "06c85365a03fdaf699966cc1d3cf53ea" 798 | }, 799 | "rversions": { 800 | "Package": "rversions", 801 | "Version": "2.1.1", 802 | "Source": "Repository", 803 | "Repository": "RSPM", 804 | "Hash": "f88fab00907b312f8b23ec13e2d437cb" 805 | }, 806 | "sass": { 807 | "Package": "sass", 808 | "Version": "0.4.0", 809 | "Source": "Repository", 810 | "Repository": "RSPM", 811 | "Hash": "50cf822feb64bb3977bda0b7091be623" 812 | }, 813 | "scales": { 814 | "Package": "scales", 815 | "Version": "1.1.1", 816 | "Source": "Repository", 817 | "Repository": "RSPM", 818 | "Hash": "6f76f71042411426ec8df6c54f34e6dd" 819 | }, 820 | "sessioninfo": { 821 | "Package": "sessioninfo", 822 | "Version": "1.1.1", 823 | "Source": "Repository", 824 | "Repository": "RSPM", 825 | "Hash": "308013098befe37484df72c39cf90d6e" 826 | }, 827 | "shiny": { 828 | "Package": "shiny", 829 | "Version": "1.6.0", 830 | "Source": "Repository", 831 | "Repository": "RSPM", 832 | "Hash": "6e3b6ae7fe02b5859e4bb277f218b8ae" 833 | }, 834 | "shinyjs": { 835 | "Package": "shinyjs", 836 | "Version": "2.0.0", 837 | "Source": "Repository", 838 | "Repository": "RSPM", 839 | "Hash": "9ddfc91d4280eaa34c2103951538976f" 840 | }, 841 | "shinylogs": { 842 | "Package": "shinylogs", 843 | "Version": "0.1.7", 844 | "Source": "Repository", 845 | "Repository": "RSPM", 846 | "Hash": "655ced208106524a1469326972d90d9a" 847 | }, 848 | "sourcetools": { 849 | "Package": "sourcetools", 850 | "Version": "0.1.7", 851 | "Source": "Repository", 852 | "Repository": "RSPM", 853 | "Hash": "947e4e02a79effa5d512473e10f41797" 854 | }, 855 | "stringi": { 856 | "Package": "stringi", 857 | "Version": "1.6.2", 858 | "Source": "Repository", 859 | "Repository": "RSPM", 860 | "Hash": "9df5e6f9a7fa11b84adf0429961de66a" 861 | }, 862 | "stringr": { 863 | "Package": "stringr", 864 | "Version": "1.4.0", 865 | "Source": "Repository", 866 | "Repository": "RSPM", 867 | "Hash": "0759e6b6c0957edb1311028a49a35e76" 868 | }, 869 | "sys": { 870 | "Package": "sys", 871 | "Version": "3.4", 872 | "Source": "Repository", 873 | "Repository": "RSPM", 874 | "Hash": "b227d13e29222b4574486cfcbde077fa" 875 | }, 876 | "systemfonts": { 877 | "Package": "systemfonts", 878 | "Version": "1.0.2", 879 | "Source": "Repository", 880 | "Repository": "RSPM", 881 | "Hash": "f2e17ba09737e2e7e2ec40fc1f9b6e08" 882 | }, 883 | "testthat": { 884 | "Package": "testthat", 885 | "Version": "3.0.2", 886 | "Source": "Repository", 887 | "Repository": "RSPM", 888 | "Hash": "495e0434d9305716b6a87031570ce109" 889 | }, 890 | "tibble": { 891 | "Package": "tibble", 892 | "Version": "3.1.2", 893 | "Source": "Repository", 894 | "Repository": "RSPM", 895 | "Hash": "349b40a9f144516d537c875e786ec8b8" 896 | }, 897 | "tidyr": { 898 | "Package": "tidyr", 899 | "Version": "1.1.3", 900 | "Source": "Repository", 901 | "Repository": "RSPM", 902 | "Hash": "450d7dfaedde58e28586b854eeece4fa" 903 | }, 904 | "tidyselect": { 905 | "Package": "tidyselect", 906 | "Version": "1.1.1", 907 | "Source": "Repository", 908 | "Repository": "RSPM", 909 | "Hash": "7243004a708d06d4716717fa1ff5b2fe" 910 | }, 911 | "tinytex": { 912 | "Package": "tinytex", 913 | "Version": "0.32", 914 | "Source": "Repository", 915 | "Repository": "RSPM", 916 | "Hash": "db9a6f2cf147751322d22c9f6647c7bd" 917 | }, 918 | "toastui": { 919 | "Package": "toastui", 920 | "Version": "0.1.2.9300", 921 | "Source": "GitHub", 922 | "RemoteType": "github", 923 | "RemoteHost": "api.github.com", 924 | "RemoteUsername": "dreamRs", 925 | "RemoteRepo": "toastui", 926 | "RemoteRef": "master", 927 | "RemoteSha": "89ae28f627c9e7459ab40a8e45f6fe6cfc8e2c4e", 928 | "Hash": "df193d340c7b4174a50f44ebaf49e826" 929 | }, 930 | "twitchr": { 931 | "Package": "twitchr", 932 | "Version": "0.1.0", 933 | "Source": "GitHub", 934 | "RemoteType": "github", 935 | "RemoteHost": "api.github.com", 936 | "RemoteUsername": "KoderKow", 937 | "RemoteRepo": "twitchr", 938 | "RemoteRef": "master", 939 | "RemoteSha": "34e99050280663663085ea133ac9f5adeb810253", 940 | "Hash": "d0db733084dbd1a11d5c8b51e859f73c" 941 | }, 942 | "tzdb": { 943 | "Package": "tzdb", 944 | "Version": "0.1.1", 945 | "Source": "Repository", 946 | "Repository": "RSPM", 947 | "Hash": "d60ee49eac3f3aaead137af987c92ddb" 948 | }, 949 | "usethis": { 950 | "Package": "usethis", 951 | "Version": "2.0.1", 952 | "Source": "Repository", 953 | "Repository": "RSPM", 954 | "Hash": "360e904f9e623286e1a0c6ca0a98c5f6" 955 | }, 956 | "utf8": { 957 | "Package": "utf8", 958 | "Version": "1.2.1", 959 | "Source": "Repository", 960 | "Repository": "RSPM", 961 | "Hash": "c3ad47dc6da0751f18ed53c4613e3ac7" 962 | }, 963 | "vctrs": { 964 | "Package": "vctrs", 965 | "Version": "0.3.8", 966 | "Source": "Repository", 967 | "Repository": "RSPM", 968 | "Hash": "ecf749a1b39ea72bd9b51b76292261f1" 969 | }, 970 | "viridisLite": { 971 | "Package": "viridisLite", 972 | "Version": "0.4.0", 973 | "Source": "Repository", 974 | "Repository": "RSPM", 975 | "Hash": "55e157e2aa88161bdb0754218470d204" 976 | }, 977 | "vroom": { 978 | "Package": "vroom", 979 | "Version": "1.5.4", 980 | "Source": "Repository", 981 | "Repository": "RSPM", 982 | "Hash": "1a23013f39e67bb57cbda6f4ddde5470" 983 | }, 984 | "waldo": { 985 | "Package": "waldo", 986 | "Version": "0.2.5", 987 | "Source": "Repository", 988 | "Repository": "RSPM", 989 | "Hash": "20c45f1d511a3f730b7b469f4d11e104" 990 | }, 991 | "whisker": { 992 | "Package": "whisker", 993 | "Version": "0.4", 994 | "Source": "Repository", 995 | "Repository": "RSPM", 996 | "Hash": "ca970b96d894e90397ed20637a0c1bbe" 997 | }, 998 | "withr": { 999 | "Package": "withr", 1000 | "Version": "2.4.2", 1001 | "Source": "Repository", 1002 | "Repository": "RSPM", 1003 | "Hash": "ad03909b44677f930fa156d47d7a3aeb" 1004 | }, 1005 | "xfun": { 1006 | "Package": "xfun", 1007 | "Version": "0.23", 1008 | "Source": "Repository", 1009 | "Repository": "RSPM", 1010 | "Hash": "791a57f43c887111490851dcd166d344" 1011 | }, 1012 | "xml2": { 1013 | "Package": "xml2", 1014 | "Version": "1.3.2", 1015 | "Source": "Repository", 1016 | "Repository": "RSPM", 1017 | "Hash": "d4d71a75dd3ea9eb5fa28cc21f9585e2" 1018 | }, 1019 | "xopen": { 1020 | "Package": "xopen", 1021 | "Version": "1.0.0", 1022 | "Source": "Repository", 1023 | "Repository": "RSPM", 1024 | "Hash": "6c85f015dee9cc7710ddd20f86881f58" 1025 | }, 1026 | "xtable": { 1027 | "Package": "xtable", 1028 | "Version": "1.8-4", 1029 | "Source": "Repository", 1030 | "Repository": "RSPM", 1031 | "Hash": "b8acdf8af494d9ec19ccb2481a9b11c2" 1032 | }, 1033 | "yaml": { 1034 | "Package": "yaml", 1035 | "Version": "2.2.1", 1036 | "Source": "Repository", 1037 | "Repository": "RSPM", 1038 | "Hash": "2826c5d9efb0a88f657c7a679c7106db" 1039 | }, 1040 | "zip": { 1041 | "Package": "zip", 1042 | "Version": "2.2.0", 1043 | "Source": "Repository", 1044 | "Repository": "RSPM", 1045 | "Hash": "c7eef2996ac270a18c2715c997a727c5" 1046 | }, 1047 | "zoo": { 1048 | "Package": "zoo", 1049 | "Version": "1.8-9", 1050 | "Source": "Repository", 1051 | "Repository": "RSPM", 1052 | "Hash": "035d1c7c12593038c26fb1c2fd40c4d2" 1053 | } 1054 | } 1055 | } 1056 | --------------------------------------------------------------------------------