12 Commits
v.1.0 ... v.1.2

99 changed files with 224 additions and 71 deletions

1
.gitignore vendored
View File

@@ -4,3 +4,4 @@ extracted/
custom_images/ custom_images/
*.txt *.txt
selected/ selected/
__pycache__

View File

@@ -1,3 +1,14 @@
# Version 1.2
- Compatibility with Cinnamon 4.8 and higher
- Notification on first start
- Bugfix: No more restart needed after first enable
- Load at first start a predefined dynamic wallpaper
- More informations in the settings
# Version 1.1
- Compatibility with Cinnamon 5.4 and 5.8
- Two new image sets
# Version 1.0 # Version 1.0
- Offline sun time calculation - Offline sun time calculation
- Online location estimation or manual input - Online location estimation or manual input

View File

@@ -6,15 +6,28 @@
Based on a location, this extension calculates the periods of a day and switches the background image of your Cinnamon desktop. The extension offers the choice between a set of included wallpapers or to select a HEIC-file. Based on a location, this extension calculates the periods of a day and switches the background image of your Cinnamon desktop. The extension offers the choice between a set of included wallpapers or to select a HEIC-file.
### Features ### Features
- 6 included image sets - 8 included image sets
- 9 day periods - 9 day periods
- HEIF converter - HEIF converter
- Image configuration assistent with simple one-click setup for image choose - Image configuration assistent with simple one-click setup for image choose
- Online location estimation - Online location estimation or offline with manual latitude and longitude input
- Offline sun angles estimation - Offline sun angles estimation
### Tested Cinnamon versions ### Tested Cinnamon versions
- 5.6 - 4.8 (Mint 20.1)
- 5.0 (Mint 20.2)
- 5.2 (Mint 20.3)
- 5.4 (Mint 21)
- 5.6 (Mint 21.1)
- 5.8 (Mint 21.2)
### Technology
- Using `JavaScript` for
- Sun angle estimation
- Location estimation
- Change of the desktop wallpapers
- `Python` displays the Image Configurator
- Image Configurator UI was written with `Glade`
## Installation ## Installation
### From the repo ### From the repo
@@ -23,6 +36,8 @@ Based on a location, this extension calculates the periods of a day and switches
3. Copy the folder `cinnamon-dynamic-wallpaper@TobiZog` to `~/.local/share/cinnamon/extensions/` 3. Copy the folder `cinnamon-dynamic-wallpaper@TobiZog` to `~/.local/share/cinnamon/extensions/`
### From Built-in Extension Manager ### From Built-in Extension Manager
![](res/download-manager.png)
1. Open "Extensions" in Linux Mint or any other distribution with Cinnamon as Desktop Environment 1. Open "Extensions" in Linux Mint or any other distribution with Cinnamon as Desktop Environment
2. Click on "Download" 2. Click on "Download"
3. Search and download it 3. Search and download it
@@ -46,6 +61,10 @@ The image sets are from https://github.com/adi1090x/dynamic-wallpaper
| ------ | ----- | ------ | | ------ | ----- | ------ |
| ![](cinnamon-dynamic-wallpaper@TobiZog/images/included_image_sets/aurora/5.jpg) | ![](cinnamon-dynamic-wallpaper@TobiZog/images/included_image_sets/beach/4.jpg) | ![](cinnamon-dynamic-wallpaper@TobiZog/images/included_image_sets/bitday/4.jpg) | | ![](cinnamon-dynamic-wallpaper@TobiZog/images/included_image_sets/aurora/5.jpg) | ![](cinnamon-dynamic-wallpaper@TobiZog/images/included_image_sets/beach/4.jpg) | ![](cinnamon-dynamic-wallpaper@TobiZog/images/included_image_sets/bitday/4.jpg) |
| Lakeside | Mountains | Sahara | | Cliffs | Gradient | Lakeside |
| -------- | --------- | ------ | | -------- | --------- | ------ |
| ![](cinnamon-dynamic-wallpaper@TobiZog/images/included_image_sets/lakeside/4.jpg) | ![](cinnamon-dynamic-wallpaper@TobiZog/images/included_image_sets/mountains/4.jpg) | ![](cinnamon-dynamic-wallpaper@TobiZog/images/included_image_sets/sahara/4.jpg) | | ![](cinnamon-dynamic-wallpaper@TobiZog/images/included_image_sets/cliffs/4.jpg) | ![](cinnamon-dynamic-wallpaper@TobiZog/images/included_image_sets/gradient/4.jpg) | ![](cinnamon-dynamic-wallpaper@TobiZog/images/included_image_sets/lakeside/4.jpg) |
| Mountains | Sahara |
| --------- | ------ |
| ![](cinnamon-dynamic-wallpaper@TobiZog/images/included_image_sets/mountains/4.jpg) | ![](cinnamon-dynamic-wallpaper@TobiZog/images/included_image_sets/sahara/4.jpg) |

View File

@@ -0,0 +1 @@
../5.4/extension.js

View File

@@ -0,0 +1 @@
../5.4/icon/

View File

@@ -0,0 +1 @@
../5.4/icons/icon.png

View File

@@ -0,0 +1 @@
../5.4/icons/

View File

@@ -0,0 +1 @@
../../5.4/image-configurator/data/

View File

@@ -0,0 +1 @@
../../5.4/image-configurator/image-configurator.glade

View File

@@ -0,0 +1,6 @@
import os
import windowHandler
if __name__ == "__main__":
wh = windowHandler.WindowHandler(os.path.expanduser("~") + "/.cinnamon/configs/cinnamon-dynamic-wallpaper@TobiZog/cinnamon-dynamic-wallpaper@TobiZog.json")
wh.showMainWindow()

View File

@@ -0,0 +1 @@
../../5.4/image-configurator/windowHandler.py

View File

@@ -0,0 +1 @@
../5.4/images/

View File

@@ -0,0 +1 @@
../5.4/scripts/

View File

@@ -0,0 +1 @@
../5.4/settings-schema.json

View File

@@ -73,12 +73,42 @@ CinnamonDynamicWallpaperExtension.prototype = {
this.bindSettings("etr_img_night", "img_night", this.setImageToTime) this.bindSettings("etr_img_night", "img_night", this.setImageToTime)
this.bindSettings("tv_times", "tvTimes") this.bindSettings("tv_times", "tvTimes")
// Check for the first startup
if (this.settings.getValue("first_start")) {
// Welcome notification
this.showNotification("Welcome to Cinnamon Dynamic Wallpaper",
"Check the preferences to choose a dynamic wallpaper", true)
// Hide the notification on system restart
this.settings.setValue("first_start", false)
// Create the folder for the selected images
Util.spawnCommandLine("mkdir " + DIRECTORY.path + "/images/selected/")
// Link the default wallpaper to the folder
for (let i = 1; i <= 9; i++) {
Util.spawnCommandLine("ln -s " +
DIRECTORY.path + "/images/included_image_sets/lakeside/" + i + ".jpg " +
DIRECTORY.path + "/images/selected/" + i + ".jpg");
}
}
// Set image initial at desktop wallpaper
this.setImageToTime() this.setImageToTime()
// Start the main loop, checks in fixed time periods the
this._loop() this._loop()
}, },
/**
* Binding the settings objects
*
* @param {*} ui_name Name of preference in settings-schema.json
* @param {*} js_name Name of preference in JavaScript
* @param {*} func Function to call on change
*/
bindSettings: function (ui_name, js_name, func = this.on_settings_changed) { bindSettings: function (ui_name, js_name, func = this.on_settings_changed) {
this.settings.bindProperty( this.settings.bindProperty(
Settings.BindingDirection.IN, Settings.BindingDirection.IN,
@@ -88,13 +118,13 @@ CinnamonDynamicWallpaperExtension.prototype = {
) )
}, },
/** /**
* Displaying a desktop notification * Displaying a desktop notification
* *
* @param {string} title The Title in the notification * @param {string} title The Title in the notification
* @param {string} text The text in the notification * @param {string} text The text in the notification
* @param {boolean} showOpenSettings Display the "Open settings" button in the notification, defaults to false * @param {boolean} showOpenSettings Display the "Open settings" button in the notification,
* defaults to false
*/ */
showNotification: function (title, text, showOpenSettings = false) { showNotification: function (title, text, showOpenSettings = false) {
let source = new MessageTray.Source(this.uuid); let source = new MessageTray.Source(this.uuid);
@@ -140,7 +170,9 @@ CinnamonDynamicWallpaperExtension.prototype = {
}, },
/**
* Estimate the right image based on time period of the day
*/
setImageToTime: function() { setImageToTime: function() {
let times = suntimes.calcTimePeriod(this.latitude, this.longitude) let times = suntimes.calcTimePeriod(this.latitude, this.longitude)
let now = new Date() let now = new Date()
@@ -159,7 +191,6 @@ CinnamonDynamicWallpaperExtension.prototype = {
for(let i = 0; i < timesArray.length; i++) { for(let i = 0; i < timesArray.length; i++) {
if(timesArray[i][0] <= now && now <= timesArray[i][1] && i != lastDayTime) { if(timesArray[i][0] <= now && now <= timesArray[i][1] && i != lastDayTime) {
global.log(PATH + "/res/images/selected/" + imageSet[i])
this.changeWallpaper("file://" + PATH + "/images/selected/" + imageSet[i]) this.changeWallpaper("file://" + PATH + "/images/selected/" + imageSet[i])
lastDayTime = i lastDayTime = i
@@ -184,7 +215,10 @@ CinnamonDynamicWallpaperExtension.prototype = {
"\nNight:\t\t\t\t" + convertToTimeString(timesArray[8][0]) + " - " + convertToTimeString(timesArray[8][1]) "\nNight:\t\t\t\t" + convertToTimeString(timesArray[8][0]) + " - " + convertToTimeString(timesArray[8][1])
}, },
/**
* Get the location of the user
* Callback for changes in preferences
*/
updateLocation: function () { updateLocation: function () {
if (this.autolocation) { if (this.autolocation) {
let loc = location.estimateLocation() let loc = location.estimateLocation()
@@ -203,26 +237,6 @@ CinnamonDynamicWallpaperExtension.prototype = {
}, },
/******************** UI Callbacks ********************/
/**
* Callback for settings-schema
* Opens the external heic-importer window
*/
openImageConfigurator: function() {
Util.spawnCommandLine("/usr/bin/env python3 " + DIRECTORY.path + "/image-configurator/image-configurator.py");
},
/**
* Callback for settings-schema
* Opens the browser and navigate to the URL of the respository
*/
openRepoWebsite: function() {
Util.spawnCommandLine("xdg-open https://github.com/TobiZog/cinnamon-dynamic-wallpaper");
},
/** /**
* Main loop * Main loop
*/ */
@@ -238,6 +252,45 @@ CinnamonDynamicWallpaperExtension.prototype = {
// Refresh every 60 seconds // Refresh every 60 seconds
Mainloop.timeout_add_seconds(60, Lang.bind(this, this._loop)); Mainloop.timeout_add_seconds(60, Lang.bind(this, this._loop));
} }
},
/******************** UI Callbacks ********************/
/**
* Callback for settings-schema
* Opens the external image configurator window
*/
openImageConfigurator: function() {
Util.spawnCommandLine("/usr/bin/env python3 " +
DIRECTORY.path + "/image-configurator/image-configurator.py");
},
/**
* Callback for settings-schema
* Opens the browser and navigates to the URL of the respository
*/
openRepoWebsite: function() {
Util.spawnCommandLine("xdg-open https://github.com/TobiZog/cinnamon-dynamic-wallpaper");
},
/**
* Callback for settings-schema
* Opens the browser and navigates to the URL of the Cinnamon Spices extension
*/
openSpicesWebsite: function() {
Util.spawnCommandLine("xdg-open https://cinnamon-spices.linuxmint.com/extensions/view/97")
},
/**
* Callback for settings-schema
* Opens the browser and navigates to the GitHub issue page
*/
openIssueWebsite: function() {
Util.spawnCommandLine("xdg-open https://github.com/TobiZog/cinnamon-dynamic-wallpaper/issues/new")
} }
} }
@@ -266,13 +319,6 @@ function enable() {
Util.spawnCommandLine("apturl apt://libheif-examples"); Util.spawnCommandLine("apturl apt://libheif-examples");
} }
// Display the welcome notification on activation
// extension.showNotification(
// APPNAME,
// "Welcome to " + APPNAME + "! Open the settings and configure the extensions.",
// true
// );
return extension; return extension;
} }

View File

@@ -0,0 +1 @@
icons/icon.png

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,6 @@
from enum import Enum
class Source(Enum):
SELECTED = 0 # Load previous selected images
EXTRACT = 1 # Use a custom image set from a heic file
SET = 2 # Use an included image set

View File

@@ -853,7 +853,7 @@
<property name="window-position">center</property> <property name="window-position">center</property>
<property name="default-width">1024</property> <property name="default-width">1024</property>
<property name="default-height">768</property> <property name="default-height">768</property>
<property name="icon">../icons/icon.png</property> <property name="icon">../../icon.png</property>
<signal name="destroy" handler="onDestroy" swapped="no"/> <signal name="destroy" handler="onDestroy" swapped="no"/>
<child> <child>
<object class="GtkStack" id="stack_main"> <object class="GtkStack" id="stack_main">

View File

@@ -0,0 +1,6 @@
import os
import windowHandler
if __name__ == "__main__":
wh = windowHandler.WindowHandler(os.path.expanduser("~") + "/.config/cinnamon/spices/cinnamon-dynamic-wallpaper@TobiZog/cinnamon-dynamic-wallpaper@TobiZog.json")
wh.showMainWindow()

View File

@@ -1,28 +1,25 @@
import gi, os, glob, json, shutil, enum, threading import gi, os, glob, json, shutil, threading, subprocess
from data.enum import Source
gi.require_version("Gtk", "3.0") gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, GdkPixbuf from gi.repository import Gtk, GdkPixbuf
PROJECT_DIR = os.path.dirname(os.path.dirname(__file__)) + "/" CONFIGURATOR_DIR = os.path.dirname(os.path.abspath(__file__))
UI_PATH = PROJECT_DIR + "image-configurator/" + "image-configurator.glade" PROJECT_DIR = os.path.dirname(CONFIGURATOR_DIR) + "/"
UI_PATH = CONFIGURATOR_DIR + "/" + "image-configurator.glade"
IMAGE_DIR = PROJECT_DIR + "images/" IMAGE_DIR = PROJECT_DIR + "images/"
IMAGE_EXTRACT_DIR = IMAGE_DIR + "extracted/" IMAGE_EXTRACT_DIR = IMAGE_DIR + "extracted/"
IMAGE_SETS_DIR = IMAGE_DIR + "included_image_sets/" IMAGE_SETS_DIR = IMAGE_DIR + "included_image_sets/"
IMAGE_SELECTED_DIR = IMAGE_DIR + "selected/" IMAGE_SELECTED_DIR = IMAGE_DIR + "selected/"
PREF_PATH = os.path.expanduser("~") + \
"/.config/cinnamon/spices/cinnamon-dynamic-wallpaper@TobiZog/cinnamon-dynamic-wallpaper@TobiZog.json"
class Source(enum.Enum): class WindowHandler:
SELECTED = 0 # Load previous selected images def __init__(self, pref_path: str) -> None:
EXTRACT = 1 # Use a custom image set from a heic file
SET = 2 # Use an included image set
class ImageConfigurator:
def __init__(self) -> None:
########### Class variables ########### ########### Class variables ###########
self.pref_path = pref_path
self.pref_vars = [ self.pref_vars = [
"etr_img_morning_twilight", "etr_img_morning_twilight",
"etr_img_sunrise", "etr_img_sunrise",
@@ -39,6 +36,8 @@ class ImageConfigurator:
"aurora", "aurora",
"beach", "beach",
"bitday", "bitday",
"cliffs",
"gradient",
"lakeside", "lakeside",
"mountains", "mountains",
"sahara" "sahara"
@@ -107,7 +106,6 @@ class ImageConfigurator:
self.image_source = Source.SELECTED self.image_source = Source.SELECTED
# Load preferences # Load preferences
self.loadFromSettings() self.loadFromSettings()
@@ -127,8 +125,9 @@ class ImageConfigurator:
def loadFromSettings(self): def loadFromSettings(self):
""" Load preferences from the Cinnamon preference file """ Load preferences from the Cinnamon preference file
""" """
#try:
# Load the settings # Load the settings
with open(PREF_PATH, "r") as pref_file: with open(self.pref_path, "r") as pref_file:
pref_data = json.load(pref_file) pref_data = json.load(pref_file)
@@ -161,13 +160,15 @@ class ImageConfigurator:
self.cb_previews[i].set_active(j) self.cb_previews[i].set_active(j)
else: else:
self.image_source = Source.SET self.image_source = Source.SET
#except:
# pass
def writeToSettings(self): def writeToSettings(self):
""" Save preferences to the Cinnamon preference file """ Save preferences to the Cinnamon preference file
""" """
# Load the settings # Load the settings
with open(PREF_PATH, "r") as pref_file: with open(self.pref_path, "r") as pref_file:
pref_data = json.load(pref_file) pref_data = json.load(pref_file)
@@ -185,7 +186,7 @@ class ImageConfigurator:
# Write the settings # Write the settings
with open(PREF_PATH, "w") as pref_file: with open(self.pref_path, "w") as pref_file:
json.dump(pref_data, pref_file, separators=(',', ':'), indent=4) json.dump(pref_data, pref_file, separators=(',', ':'), indent=4)
@@ -364,8 +365,3 @@ class ImageConfigurator:
""" UI signal if the window is closed by the user """ UI signal if the window is closed by the user
""" """
Gtk.main_quit() Gtk.main_quit()
if __name__ == "__main__":
ic = ImageConfigurator()
ic.showMainWindow()

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 293 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

View File

@@ -19,9 +19,12 @@
"title": "About", "title": "About",
"sections": [ "sections": [
"sec_project", "sec_project",
"sec_github" "sec_github",
"sec_report_issue"
] ]
}, },
"sec_image_configuration": { "sec_image_configuration": {
"type": "section", "type": "section",
"title": "Image set", "title": "Image set",
@@ -52,7 +55,9 @@
"title": "About the project", "title": "About the project",
"keys": [ "keys": [
"lb_about", "lb_about",
"lb_author" "lb_author",
"lb_spices",
"btn_spices"
] ]
}, },
"sec_github": { "sec_github": {
@@ -62,8 +67,18 @@
"lb_repository", "lb_repository",
"btn_open_repository" "btn_open_repository"
] ]
},
"sec_report_issue": {
"type": "section",
"title": "Report an issue",
"keys": [
"lb_report_issue",
"btn_report_issue"
]
} }
}, },
"lb_image_configuration": { "lb_image_configuration": {
"type": "label", "type": "label",
"description": "Choose an included image set or import a heic-file with the Image Configurator" "description": "Choose an included image set or import a heic-file with the Image Configurator"
@@ -104,6 +119,8 @@
"description": "Time sections today", "description": "Time sections today",
"default": "" "default": ""
}, },
"lb_about": { "lb_about": {
"type": "label", "type": "label",
"description": "Based on a location, this extension calculates the periods of a day and switch the background image of your Cinnamon desktop. The extension offers the choice between a set of predownloaded wallpapers or to select a custom set of images." "description": "Based on a location, this extension calculates the periods of a day and switch the background image of your Cinnamon desktop. The extension offers the choice between a set of predownloaded wallpapers or to select a custom set of images."
@@ -112,6 +129,16 @@
"type": "label", "type": "label",
"description": "Developed by TobiZog" "description": "Developed by TobiZog"
}, },
"lb_spices": {
"type": "label",
"description": "If you want more information or rate the extension, you can visit the site Cinnamon Spices Website."
},
"btn_spices": {
"type": "button",
"description": "Cinnamon Dynamic Wallpaper at Cinnamon Spices Website",
"callback": "openSpicesWebsite"
},
"lb_repository": { "lb_repository": {
"type": "label", "type": "label",
"description": "This project is Open Source. You can visit the whole source code of this extension on GitHub" "description": "This project is Open Source. You can visit the whole source code of this extension on GitHub"
@@ -121,54 +148,71 @@
"description": "Open the Repository", "description": "Open the Repository",
"callback": "openRepoWebsite" "callback": "openRepoWebsite"
}, },
"lb_report_issue": {
"type": "label",
"description": "Do you find an issue? Or want a new feature? Go to the GitHub repository and create a new issue."
},
"btn_report_issue": {
"type": "button",
"description": "Submit an Issue",
"callback": "openIssueWebsite"
},
"etr_choosen_image_set": { "etr_choosen_image_set": {
"type": "entry", "type": "entry",
"default": "", "default": "lakeside",
"description": "" "description": ""
}, },
"etr_img_morning_twilight": { "etr_img_morning_twilight": {
"type": "entry", "type": "entry",
"default": "", "default": "1.jpg",
"description": "" "description": ""
}, },
"etr_img_sunrise": { "etr_img_sunrise": {
"type": "entry", "type": "entry",
"default": "", "default": "2.jpg",
"description": "" "description": ""
}, },
"etr_img_morning": { "etr_img_morning": {
"type": "entry", "type": "entry",
"default": "", "default": "3.jpg",
"description": "" "description": ""
}, },
"etr_img_noon": { "etr_img_noon": {
"type": "entry", "type": "entry",
"default": "", "default": "4.jpg",
"description": "" "description": ""
}, },
"etr_img_afternoon": { "etr_img_afternoon": {
"type": "entry", "type": "entry",
"default": "", "default": "5.jpg",
"description": "" "description": ""
}, },
"etr_img_evening": { "etr_img_evening": {
"type": "entry", "type": "entry",
"default": "", "default": "6.jpg",
"description": "" "description": ""
}, },
"etr_img_sunset": { "etr_img_sunset": {
"type": "entry", "type": "entry",
"default": "", "default": "7.jpg",
"description": "" "description": ""
}, },
"etr_img_night_twilight": { "etr_img_night_twilight": {
"type": "entry", "type": "entry",
"default": "", "default": "8.jpg",
"description": "" "description": ""
}, },
"etr_img_night": { "etr_img_night": {
"type": "entry", "type": "entry",
"default": "", "default": "9.jpg",
"description": "" "description": ""
},
"first_start": {
"type": "generic",
"default": true
} }
} }

View File

@@ -1 +1 @@
icons/icon.png 5.4/icons/icon.png

View File

@@ -2,9 +2,15 @@
"uuid": "cinnamon-dynamic-wallpaper@TobiZog", "uuid": "cinnamon-dynamic-wallpaper@TobiZog",
"name": "Cinnamon Dynamic Wallpaper", "name": "Cinnamon Dynamic Wallpaper",
"description": "Cinnamon extension for dynamic desktop backgrounds based on time and location", "description": "Cinnamon extension for dynamic desktop backgrounds based on time and location",
"version": "1.0", "version": "1.2",
"multiversion": true,
"cinnamon-version": [ "cinnamon-version": [
"5.6" "4.8",
"5.0",
"5.2",
"5.4",
"5.6",
"5.8"
], ],
"max-instances": 1, "max-instances": 1,
"url": "https://github.com/TobiZog/cinnamon-dynamic-wallpaper" "url": "https://github.com/TobiZog/cinnamon-dynamic-wallpaper"

BIN
res/download-manager.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB