Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 40 additions & 1 deletion .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -301,8 +301,38 @@ jobs:
release/CCloud-*-linux.AppImage
release/ccloud.deb
release/ccloud.rpm
build-webos:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'

- name: Install webOS CLI
run: npm install -g @webos-tools/cli

- name: Build webOS package
run: |
# webOS versions are x.y.z, so drop the build number from pubspec.yaml
VERSION=$(grep '^version:' pubspec.yaml | cut -d ':' -f 2 | tr -d ' ' | cut -d '+' -f 1)
node -e "const f='webos/app/appinfo.json',a=require('./'+f);a.version='$VERSION';require('fs').writeFileSync(f,JSON.stringify(a,null,2)+'\n')"
mkdir -p webos/dist release
ares-package webos/app -o webos/dist --no-minify
cp webos/dist/com.pira.ccloud_${VERSION}_all.ipk release/CCloud-webos.ipk
shell: bash

- name: Upload webOS artifacts
uses: actions/upload-artifact@v4
with:
name: webos-build
path: release/CCloud-webos.ipk

release:
needs: [build-windows, build-macos, build-linux]
needs: [build-windows, build-macos, build-linux, build-webos]
runs-on: ubuntu-latest
steps:
- name: Checkout code
Expand All @@ -323,6 +353,11 @@ jobs:
with:
name: linux-build

- name: Download webOS artifacts
uses: actions/download-artifact@v4
with:
name: webos-build

- name: Get tag name
id: get_tag
run: |
Expand Down Expand Up @@ -353,6 +388,7 @@ jobs:
CCloud-*-linux.AppImage
ccloud.deb
ccloud.rpm
CCloud-webos.ipk
body: |
## CCloud Release ${{ env.TAG_NAME }}

Expand All @@ -374,6 +410,9 @@ jobs:
| Linux | x64 (AppImage) | <a href="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/code3-dev/CCloud-GUI/releases/download/${{ env.TAG_NAME }}/CCloud-${{ env.VERSION }}-linux.AppImage"><img src="https://img.shields.io/badge/Linux-AppImage-FCC624?style=for-the-badge&logo=linux&logoColor=black" alt="Linux AppImage"></a> |
| Linux | x64 (.deb) | <a href="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/code3-dev/CCloud-GUI/releases/download/${{ env.TAG_NAME }}/ccloud.deb"><img src="https://img.shields.io/badge/Linux-.deb-FCC624?style=for-the-badge&logo=debian&logoColor=white" alt="Linux .deb"></a> |
| Linux | x64 (.rpm) | <a href="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/code3-dev/CCloud-GUI/releases/download/${{ env.TAG_NAME }}/ccloud.rpm"><img src="https://img.shields.io/badge/Linux-.rpm-FCC624?style=for-the-badge&logo=redhat&logoColor=white" alt="Linux .rpm"></a> |
| LG webOS TV | webOS 4.0+ (.ipk) | <a href="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/code3-dev/CCloud-GUI/releases/download/${{ env.TAG_NAME }}/CCloud-webos.ipk"><img src="https://img.shields.io/badge/LG_webOS-.ipk-A50034?style=for-the-badge&logo=lg&logoColor=white" alt="LG webOS TV .ipk"></a> |
| Android | Universal APK | <a href="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/code3-dev/CCloud/releases/latest/download/CCloud-universal.apk"><img src="https://img.shields.io/badge/Android-APK-3DDC84?style=for-the-badge&logo=android&logoColor=white" alt="Android Universal APK"></a> |

For detailed instructions on installing and running CCloud on Linux, please refer to our [Linux Installation Guide](https://github.com/code3-dev/CCloud-GUI/blob/main/assets/LINUX_INSTALL.md).

To install CCloud on an LG TV, see the [webOS TV Installation Guide](https://github.com/code3-dev/CCloud-GUI/blob/main/assets/WEBOS_INSTALL.md).
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ CCloud features a sleek, contemporary user interface designed for optimal user e
- **Unlimited Content**: Enjoy unlimited streaming of Iranian Persian movies and series

### 🖥️ Platform Support
- **Cross-Platform**: Available for Windows, macOS, and Linux
- **Cross-Platform**: Available for Windows, macOS, Linux, and LG webOS TVs
- **Modern Interface**: Clean, intuitive, and user-friendly design

### ⚡ Performance
Expand All @@ -90,6 +90,10 @@ If you encounter any errors when installing or running CCloud on Windows, please

For detailed instructions on installing and running CCloud on Linux, please refer to our [Linux Installation Guide](assets/LINUX_INSTALL.md).

### 📺 LG webOS TV Installation

CCloud is also available for LG smart TVs as `CCloud-webos.ipk`. It's a remote-friendly TV app built with web technologies, in the [`webos/`](webos/) folder. See the [webOS TV Installation Guide](assets/WEBOS_INSTALL.md) to install it in Developer Mode.

## 💻 Development

### 🛠️ Technologies
Expand Down
38 changes: 38 additions & 0 deletions assets/WEBOS_INSTALL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# LG webOS TV Installation Guide for CCloud

CCloud for LG TVs is a `.ipk` package (`CCloud-webos.ipk` on the releases page). LG TVs only install apps from outside the LG Content Store in **Developer Mode**.

## 1. Enable Developer Mode on the TV

1. Create a free account at [developer.lge.com](https://developer.lge.com).
2. On the TV, install **Developer Mode** from the LG Content Store and sign in with that account.
3. Turn on **Dev Mode Status**, let the TV restart, and open Developer Mode again.
4. Turn on **Key Server** and note the TV's IP address shown there.

## 2. Install the app from a computer

1. Install Node.js, then the webOS CLI:
```bash
npm install -g @webos-tools/cli
```
2. Add the TV (use the IP address shown in Developer Mode):
```bash
ares-setup-device --add tv --info "{'host':'192.168.1.20','port':'9922','username':'prisoner'}"
ares-novacom --device tv --getkey # enter the passphrase shown in Developer Mode
```
3. Install and launch CCloud:
```bash
ares-install --device tv CCloud-webos.ipk
ares-launch --device tv com.pira.ccloud
```

If you prefer a graphical tool, [webOS Dev Manager](https://github.com/webosbrew/dev-manager-desktop) can install the same `.ipk`. On a rooted TV with the Homebrew Channel, you can install the `.ipk` directly.

## Keeping the app

A Developer Mode session lasts about 50 days. When it expires, apps installed this way are removed. To keep them, open the Developer Mode app and press **Extend** before the timer runs out.

## Notes

- **Remote:** use the arrows and OK to move and select, and Back to go back. In the player, OK plays or pauses, ◀/▶ seek 10 seconds, and Yellow changes the quality.
- **Video servers:** most of them only accept connections from Iran. If a video doesn't start, the app says so and you can choose another quality.
1 change: 1 addition & 0 deletions webos/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dist/
46 changes: 46 additions & 0 deletions webos/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# CCloud for LG webOS TV

A webOS TV app for CCloud. Flutter does not target webOS, so this is a small web app (plain HTML, CSS and JavaScript, no build step) that talks to the same API as the desktop app and has the same sections: Movies, Series, Search, Favorites, Countries and Settings, plus a Home screen built from the API's `first` rows. The interface is in Persian and laid out right to left, like the desktop app.

| Desktop (`lib/src/repositories`) | TV (`app/js/api.js`) |
| --- | --- |
| `BaseRepository` (main server, then the helper servers) | `call()` tries the same three servers in the same order |
| `MovieRepository`, `SeriesRepository` | `Api.movies`, `Api.series` (genre and sort filters) |
| `CountriesRepository` | `Api.countries`, `Api.posters({country})` |
| `GenreRepository`, `SearchRepository`, `SeasonsRepository` | `Api.genres`, `Api.search`, `Api.seasons` |
| `StorageUtils` (favorites) | `app/js/store.js`: favorites, watch positions and the preferred quality, in localStorage |

The TV plays MP4 and MKV files directly with its own media player. The player is built for the remote:
- The app resumes where you stopped.
- **Yellow** changes the quality.
- Audio and subtitle buttons appear when a file has several tracks.
- Series have a next-episode button.

Live TV is not included, because the TVGarden channel list that the desktop app uses currently returns 404.

## Files

```
webos/app/
appinfo.json app id com.pira.ccloud; the version follows pubspec.yaml in CI
index.html layout and Content-Security-Policy
css/app.css
js/nav.js remote-control (spatial) navigation
js/api.js CCloud API client
js/store.js favorites, positions, preferences
js/player.js video player
js/app.js screens and routing
```

## Build and run

```bash
npm install -g @webos-tools/cli
ares-package webos/app -o webos/dist # creates com.pira.ccloud_<version>_all.ipk
ares-install --device tv webos/dist/*.ipk
ares-launch --device tv com.pira.ccloud
```

You can also open `webos/app` in the [webOS TV simulator](https://webostv.developer.lge.com/develop/tools/simulator-introduction), or with `ares-launch -s <version> webos/app`. Debug on a TV with `ares-inspect --device tv com.pira.ccloud`.

See [assets/WEBOS_INSTALL.md](../assets/WEBOS_INSTALL.md) for how to set up the TV.
14 changes: 14 additions & 0 deletions webos/app/appinfo.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"id": "com.pira.ccloud",
"version": "1.0.4",
"vendor": "Hossein Pira",
"type": "web",
"main": "index.html",
"title": "CCloud",
"icon": "img/icon.png",
"largeIcon": "img/largeIcon.png",
"bgColor": "#0e1116",
"resolution": "1920x1080",
"disableBackHistoryAPI": true,
"requiredPermissions": []
}
142 changes: 142 additions & 0 deletions webos/app/css/app.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
:root {
--bg: #0e1116;
--panel: #1a1f27;
--panel-2: #262c36;
--text: #f2f4f7;
--muted: #98a0ad;
--accent: #3d8bfd;
--gold: #f5c518;
--focus: #ffffff;
--menu-w: 280px;
}
* { box-sizing: border-box; }
/* Spacing uses margins, not flex "gap", which TVs before webOS 6 lack.
Pages are RTL, so the next item in a row sits to the left. */
html, body {
margin: 0; width: 1920px; height: 1080px; overflow: hidden;
background: var(--bg); color: var(--text);
font-family: "LG Smart UI", Vazirmatn, Tahoma, "Segoe UI", sans-serif; font-size: 28px;
}
.focusable:focus { outline: none; }
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }
.hidden { display: none !important; }

/* Menu on the right (RTL). */
.menu {
.menu > * + * { margin-top: 8px; }
position: fixed; right: 0; top: 0; bottom: 0; width: var(--menu-w);
background: linear-gradient(270deg, #000 0%, rgba(0,0,0,.85) 100%);
padding: 44px 24px; display: flex; flex-direction: column; z-index: 5;
}
.brand { display: flex; align-items: center; margin: 0 10px 34px; font-size: 38px; font-weight: 800; }
.brand img { margin-left: 14px; }
.brand img { width: 64px; height: 64px; border-radius: 14px; }
.menu-item { text-align: right; padding: 15px 20px; border-radius: 12px; color: var(--muted); font-size: 30px; }
.menu-item.active { color: var(--text); }
.menu-item.focused { background: var(--text); color: #000; }

#view { position: absolute; right: var(--menu-w); top: 0; left: 0; bottom: 0; overflow: hidden; }
.scroller { position: absolute; top: 0; right: 0; bottom: 0; left: 0; overflow-y: auto; padding: 44px 40px 120px 60px; scroll-behavior: smooth; }
.scroller::-webkit-scrollbar { display: none; }
.page-title { font-size: 46px; margin: 0 8px 20px 0; }

.row { margin-bottom: 18px; }
.row h2 { font-size: 34px; margin: 0 8px 4px 0; font-weight: 700; }
.row-items { display: flex; overflow-x: auto; padding: 16px 8px 18px; scroll-behavior: smooth; align-items: flex-start; }
.row-items > * + * { margin-right: 24px; }
.row-items::-webkit-scrollbar { display: none; }

/* Poster cards, 2:3. */
.card { flex: 0 0 auto; width: 230px; border-radius: 14px; transition: transform .15s; text-align: right; }
.card .frame { position: relative; width: 230px; height: 345px; border-radius: 14px; overflow: hidden; background: var(--panel); }
.card .poster { width: 100%; height: 100%; object-fit: cover; display: block; }
.card .badge { position: absolute; top: 8px; left: 8px; background: rgba(0,0,0,.8); padding: 2px 10px; border-radius: 8px; font-size: 20px; direction: ltr; }
.card .badge b { color: var(--gold); }
.card .kind { position: absolute; bottom: 8px; right: 8px; background: var(--accent); padding: 1px 10px; border-radius: 8px; font-size: 18px; }
.card .name { font-size: 22px; margin-top: 10px; line-height: 1.3; height: 2.6em; overflow: hidden; color: var(--muted); }
.card.focused { transform: scale(1.07); }
.card.focused .frame { outline: 5px solid var(--focus); outline-offset: 2px; }
.card.focused .name { color: var(--text); }
.more { flex: 0 0 auto; width: 180px; height: 345px; border-radius: 14px; background: var(--panel); font-size: 28px; color: var(--muted); }
.more.focused { background: var(--text); color: #000; }

.grid { display: flex; flex-wrap: wrap; padding: 16px 8px; }
.grid > * { margin: 0 0 34px 24px; }
.chips { display: flex; flex-wrap: wrap; padding: 8px 8px 0; }
.chips > * { margin: 0 0 16px 16px; }
.chip { display: flex; align-items: center; padding: 14px 24px; border-radius: 12px; background: var(--panel-2); font-size: 26px; }
.chip img { margin-left: 12px; }
.chip img { width: 44px; height: 30px; object-fit: cover; border-radius: 4px; }
.chip.on { background: var(--accent); }
.chip.focused { background: var(--text); color: #000; }

.btn { padding: 16px 34px; border-radius: 12px; background: var(--panel-2); font-size: 28px; font-weight: 600; }
.btn.primary { background: var(--accent); color: #fff; }
.btn.focused { outline: 5px solid var(--focus); outline-offset: 3px; background: var(--text); color: #000; }
.actions { display: flex; flex-wrap: wrap; }
.actions > * { margin: 0 0 18px 18px; }
.filters { display: flex; margin: 0 8px 10px 0; }
.filters > * + * { margin-right: 18px; }

.search-box { display: flex; align-items: center; margin: 0 8px 24px 0; }
.search-box > * + * { margin-right: 20px; }
.input { width: 900px; font: inherit; font-size: 34px; padding: 18px 26px; border-radius: 14px; border: 3px solid #333; background: var(--panel); color: var(--text); outline: none; }
.input.focused, .input:focus { border-color: var(--focus); }
.empty, .loading { color: var(--muted); font-size: 30px; padding: 40px 8px; }

/* Details */
.details { position: relative; min-height: 900px; }
.backdrop { position: fixed; top: 0; left: 0; right: var(--menu-w); height: 760px; background-size: cover; background-position: center; opacity: .35; z-index: -1; -webkit-mask-image: linear-gradient(180deg, #000 30%, transparent 100%); }
.d-head { display: flex; align-items: flex-start; padding: 10px 8px 0 0; }
.d-poster { margin-left: 44px; }
.d-poster { width: 300px; height: 450px; object-fit: cover; border-radius: 16px; flex: 0 0 auto; background: var(--panel); box-shadow: 0 10px 40px rgba(0,0,0,.6); }
.d-info { flex: 1; min-width: 0; }
.d-info h1 { font-size: 54px; margin: 0 0 12px; line-height: 1.2; }
.d-meta { display: flex; color: #ccd; margin-bottom: 14px; flex-wrap: wrap; }
.d-meta > * { margin-left: 24px; }
.d-meta b { color: var(--gold); }
.d-desc { color: #ccd; line-height: 1.6; max-height: 6.4em; overflow: hidden; white-space: pre-line; margin: 0 0 26px; font-size: 26px; }
.section-title { font-size: 32px; margin: 34px 8px 10px 0; }
.sources .btn { direction: rtl; }
.episodes { display: flex; flex-direction: column; padding: 8px; }
.episodes > * + * { margin-top: 12px; }
.ep { display: flex; align-items: center; text-align: right; padding: 16px 24px; border-radius: 12px; background: var(--panel); font-size: 28px; }
.ep > * + * { margin-right: 20px; }
.ep .n { color: var(--muted); min-width: 60px; }
.ep > .pos { margin-right: auto; color: var(--accent); font-size: 22px; }
.ep.focused { background: var(--text); color: #000; }
.ep.focused .n, .ep.focused .pos { color: #333; }

.settings { padding: 10px 8px; max-width: 1300px; }
.settings h2 { font-size: 34px; margin: 34px 0 14px; }
.settings p, .settings li { line-height: 1.6; color: #ccd; }

.player { position: fixed; top: 0; right: 0; bottom: 0; left: 0; background: #000; z-index: 20; }
.player video { width: 100%; height: 100%; background: #000; }
.overlay { position: absolute; top: 0; right: 0; bottom: 0; left: 0; background: linear-gradient(0deg, rgba(0,0,0,.9) 0%, rgba(0,0,0,0) 40%, rgba(0,0,0,0) 75%, rgba(0,0,0,.8) 100%); }
.ov-top { position: absolute; top: 50px; left: 80px; right: 80px; }
.ov-title { font-size: 44px; font-weight: 800; }
.ov-sub { font-size: 28px; color: #ccc; margin-top: 6px; }
.ov-msg { position: absolute; top: 40%; left: 50%; transform: translate(-50%, -50%); max-width: 1300px; background: rgba(20,24,30,.95); padding: 30px 44px; border-radius: 16px; font-size: 30px; line-height: 1.6; text-align: center; }
.ov-bottom { position: absolute; left: 80px; right: 80px; bottom: 50px; }
.progress { padding: 18px 0; border-radius: 8px; }
.progress .bar { position: relative; height: 10px; background: rgba(255,255,255,.25); border-radius: 5px; overflow: hidden; }
.progress .fill { position: absolute; left: 0; top: 0; bottom: 0; background: var(--accent); z-index: 2; }
.progress .buf { position: absolute; left: 0; top: 0; bottom: 0; background: rgba(255,255,255,.35); z-index: 1; }
.progress.focused .bar { height: 18px; outline: 4px solid #fff; outline-offset: 4px; }
.times { display: flex; justify-content: space-between; font-size: 26px; color: #ddd; margin: 4px 0 20px; }
.controls { display: flex; }
.controls > * + * { margin-right: 16px; }
.ctl { padding: 14px 24px; border-radius: 12px; background: rgba(255,255,255,.14); font-size: 26px; white-space: nowrap; }
.ctl.focused { background: #fff; color: #000; }
.spinner { position: absolute; left: 50%; top: 50%; width: 90px; height: 90px; margin: -45px; border: 8px solid rgba(255,255,255,.2); border-top-color: #fff; border-radius: 50%; animation: spin 1s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }

.modal { position: fixed; top: 0; right: 0; bottom: 0; left: 0; background: rgba(0,0,0,.6); z-index: 40; display: flex; align-items: center; justify-content: flex-start; }
.modal-box { width: 560px; height: 100%; background: #151a21; padding: 60px 40px; overflow-y: auto; }
.modal-box h3 { font-size: 38px; margin: 0 0 30px; }
.opt { display: block; width: 100%; text-align: right; padding: 18px 24px; border-radius: 12px; margin-bottom: 8px; font-size: 30px; }
.opt.sel::before { content: "✓ "; color: var(--accent); }
.opt.focused { background: #fff; color: #000; }

.toast { position: fixed; left: 50%; bottom: 70px; transform: translateX(-50%); background: rgba(30,34,42,.96); padding: 18px 36px; border-radius: 14px; z-index: 60; font-size: 28px; max-width: 1400px; text-align: center; }
Binary file added webos/app/img/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added webos/app/img/largeIcon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added webos/app/img/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading